@gengjiawen/os-init 1.17.0 → 1.18.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 +14 -0
- package/README.md +20 -0
- package/bin/bin.js +38 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +4 -1
- package/build/mihomo.d.ts +8 -0
- package/build/mihomo.js +207 -0
- package/libs/index.ts +3 -0
- package/libs/mihomo.test.ts +293 -0
- package/libs/mihomo.ts +280 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.18.0](https://github.com/gengjiawen/os-init/compare/v1.17.0...v1.18.0) (2026-03-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add clash config generator ([b9f46b4](https://github.com/gengjiawen/os-init/commit/b9f46b4366955a0cfb8c6337a9956d3461d673f6))
|
|
9
|
+
* add optional mihomo downloader ([632d502](https://github.com/gengjiawen/os-init/commit/632d50234c2a6119f989dd39feb89b3d4d542514))
|
|
10
|
+
* print pm2 command for clash setup ([c4a3a81](https://github.com/gengjiawen/os-init/commit/c4a3a81eaa992816d1d54fdd019e7479bccbc2fb))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* stabilize mihomo download test across platforms ([#28](https://github.com/gengjiawen/os-init/issues/28)) ([006b748](https://github.com/gengjiawen/os-init/commit/006b7481fe5c0e60628547bb22bfa357c8502bea))
|
|
16
|
+
|
|
3
17
|
## [1.17.0](https://github.com/gengjiawen/os-init/compare/v1.16.0...v1.17.0) (2026-03-07)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -72,6 +72,26 @@ Example:
|
|
|
72
72
|
pnpx @gengjiawen/os-init set-fish
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
### Generate Mihomo Config
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpx @gengjiawen/os-init set-clash
|
|
79
|
+
pnpx @gengjiawen/os-init set-clash --download
|
|
80
|
+
pnpx @gengjiawen/os-init set-clash --target ~/mihomo/config.yml
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Generates a Mihomo `config.yml` from the built-in template. This command will:
|
|
84
|
+
|
|
85
|
+
- Write `./config.yml` in the current directory by default
|
|
86
|
+
- Download the Mihomo binary to the config directory when `--download` is set
|
|
87
|
+
- Support a custom output path with `-t, --target <path>`
|
|
88
|
+
|
|
89
|
+
After generating the config, you can keep Mihomo running with pm2:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pm2 start mihomo --name mihomo -- -f /absolute/path/to/config.yml && pm2 save
|
|
93
|
+
```
|
|
94
|
+
|
|
75
95
|
---
|
|
76
96
|
|
|
77
97
|
### Configure Claude Code
|
package/bin/bin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
|
+
const path = require('path')
|
|
4
5
|
const {
|
|
5
6
|
writeClaudeConfig,
|
|
6
7
|
installDeps,
|
|
@@ -15,11 +16,17 @@ const {
|
|
|
15
16
|
writeRaycastConfig,
|
|
16
17
|
setupDevEnvironment,
|
|
17
18
|
setupAndroidEnvironment,
|
|
19
|
+
writeMihomoConfig,
|
|
20
|
+
downloadMihomoBinary,
|
|
18
21
|
} = require('../build')
|
|
19
22
|
const { appendFishImportScript } = require('../build/fish-shell-utils')
|
|
20
23
|
|
|
21
24
|
const program = new Command()
|
|
22
25
|
|
|
26
|
+
function shellEscape(value) {
|
|
27
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
program
|
|
24
31
|
.command('set-cc')
|
|
25
32
|
.description('setup Claude Code')
|
|
@@ -255,4 +262,35 @@ program
|
|
|
255
262
|
}
|
|
256
263
|
})
|
|
257
264
|
|
|
265
|
+
program
|
|
266
|
+
.command('set-clash')
|
|
267
|
+
.description('generate clash/mihomo config.yml in current directory')
|
|
268
|
+
.option('-t, --target <path>', 'Target path for mihomo config.yml')
|
|
269
|
+
.option('--download', 'Download Mihomo binary to the config directory')
|
|
270
|
+
.action(async (options) => {
|
|
271
|
+
try {
|
|
272
|
+
const { configPath } = writeMihomoConfig(options.target)
|
|
273
|
+
const resolvedConfigPath = path.resolve(configPath)
|
|
274
|
+
let mihomoCommand = 'mihomo'
|
|
275
|
+
|
|
276
|
+
if (options.download) {
|
|
277
|
+
const { binaryPath, downloadUrl, version } = await downloadMihomoBinary(
|
|
278
|
+
path.dirname(resolvedConfigPath)
|
|
279
|
+
)
|
|
280
|
+
mihomoCommand = path.resolve(binaryPath)
|
|
281
|
+
console.log(`Mihomo download URL: ${downloadUrl}`)
|
|
282
|
+
console.log(`Mihomo version: ${version}`)
|
|
283
|
+
console.log(`Mihomo binary downloaded to: ${binaryPath}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pm2Command = `pm2 start ${shellEscape(mihomoCommand)} --name mihomo -- -f ${shellEscape(resolvedConfigPath)} && pm2 save`
|
|
287
|
+
console.log(`Clash config written to: ${configPath}`)
|
|
288
|
+
console.log('Run Mihomo with pm2:')
|
|
289
|
+
console.log(` ${pm2Command}`)
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('Failed to generate Clash config:', err.message)
|
|
292
|
+
process.exit(1)
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
|
|
258
296
|
program.parse(process.argv)
|
package/build/index.d.ts
CHANGED
|
@@ -8,3 +8,4 @@ export { writeOpencodeConfig, installOpencodeDeps } from './opencode';
|
|
|
8
8
|
export { writeAllAgentsConfig, installAllAgentsDeps } from './all-agents';
|
|
9
9
|
export { setupDevEnvironment } from './dev-setup';
|
|
10
10
|
export { setupAndroidEnvironment } from './android-setup';
|
|
11
|
+
export { writeMihomoConfig, downloadMihomoBinary } from './mihomo';
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setupAndroidEnvironment = exports.setupDevEnvironment = exports.installAllAgentsDeps = exports.writeAllAgentsConfig = exports.installOpencodeDeps = exports.writeOpencodeConfig = exports.installGeminiDeps = exports.writeGeminiConfig = exports.installCodexDeps = exports.writeCodexConfig = exports.installDeps = exports.writeClaudeConfig = void 0;
|
|
3
|
+
exports.downloadMihomoBinary = exports.writeMihomoConfig = exports.setupAndroidEnvironment = exports.setupDevEnvironment = exports.installAllAgentsDeps = exports.writeAllAgentsConfig = exports.installOpencodeDeps = exports.writeOpencodeConfig = exports.installGeminiDeps = exports.writeGeminiConfig = exports.installCodexDeps = exports.writeCodexConfig = exports.installDeps = exports.writeClaudeConfig = void 0;
|
|
4
4
|
exports.writeRaycastConfig = writeRaycastConfig;
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
@@ -57,3 +57,6 @@ var dev_setup_1 = require("./dev-setup");
|
|
|
57
57
|
Object.defineProperty(exports, "setupDevEnvironment", { enumerable: true, get: function () { return dev_setup_1.setupDevEnvironment; } });
|
|
58
58
|
var android_setup_1 = require("./android-setup");
|
|
59
59
|
Object.defineProperty(exports, "setupAndroidEnvironment", { enumerable: true, get: function () { return android_setup_1.setupAndroidEnvironment; } });
|
|
60
|
+
var mihomo_1 = require("./mihomo");
|
|
61
|
+
Object.defineProperty(exports, "writeMihomoConfig", { enumerable: true, get: function () { return mihomo_1.writeMihomoConfig; } });
|
|
62
|
+
Object.defineProperty(exports, "downloadMihomoBinary", { enumerable: true, get: function () { return mihomo_1.downloadMihomoBinary; } });
|
package/build/mihomo.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.downloadMihomoBinary = downloadMihomoBinary;
|
|
4
|
+
exports.writeMihomoConfig = writeMihomoConfig;
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const zlib_1 = require("zlib");
|
|
9
|
+
const unzip_url_1 = require("@gengjiawen/unzip-url");
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
const MIHOMO_CONFIG_TEMPLATE = `mixed-port: 7890
|
|
12
|
+
mode: rule
|
|
13
|
+
log-level: info
|
|
14
|
+
dns:
|
|
15
|
+
enable: true
|
|
16
|
+
ipv6: false
|
|
17
|
+
default-nameserver:
|
|
18
|
+
- 1.1.1.1
|
|
19
|
+
- 223.5.5.5
|
|
20
|
+
- 119.29.29.29
|
|
21
|
+
sniffer:
|
|
22
|
+
enable: true
|
|
23
|
+
sniffing:
|
|
24
|
+
- tls
|
|
25
|
+
- http
|
|
26
|
+
tun:
|
|
27
|
+
enable: true
|
|
28
|
+
stack: system
|
|
29
|
+
auto-route: true
|
|
30
|
+
auto-redirect: true
|
|
31
|
+
auto-detect-interface: true
|
|
32
|
+
proxies:
|
|
33
|
+
- name: socks5-proxy
|
|
34
|
+
type: socks5
|
|
35
|
+
server: your.ip
|
|
36
|
+
port: 6153
|
|
37
|
+
proxy-groups:
|
|
38
|
+
- name: PROXY
|
|
39
|
+
type: select
|
|
40
|
+
proxies:
|
|
41
|
+
- socks5-proxy
|
|
42
|
+
rules:
|
|
43
|
+
- IP-CIDR,127.0.0.0/8,DIRECT
|
|
44
|
+
- IP-CIDR,10.0.0.0/8,DIRECT
|
|
45
|
+
- IP-CIDR,172.16.0.0/12,DIRECT
|
|
46
|
+
- IP-CIDR,192.168.0.0/16,DIRECT
|
|
47
|
+
- IP-CIDR,169.254.0.0/16,DIRECT
|
|
48
|
+
- IP-CIDR,100.64.0.0/10,DIRECT
|
|
49
|
+
- IP-CIDR6,fc00::/7,DIRECT
|
|
50
|
+
- IP-CIDR6,fe80::/10,DIRECT
|
|
51
|
+
- DOMAIN-SUFFIX,local,DIRECT
|
|
52
|
+
- DOMAIN-SUFFIX,cn,DIRECT
|
|
53
|
+
- GEOIP,CN,DIRECT
|
|
54
|
+
- MATCH,PROXY
|
|
55
|
+
`;
|
|
56
|
+
const MIHOMO_LATEST_RELEASE_API_URL = 'https://api.github.com/repos/MetaCubeX/mihomo/releases/latest';
|
|
57
|
+
function getMihomoBinaryFilename(platform = process.platform) {
|
|
58
|
+
return platform === 'win32' ? 'mihomo.exe' : 'mihomo';
|
|
59
|
+
}
|
|
60
|
+
function getMihomoPlatformToken(platform = process.platform) {
|
|
61
|
+
switch (platform) {
|
|
62
|
+
case 'linux':
|
|
63
|
+
return 'linux';
|
|
64
|
+
case 'darwin':
|
|
65
|
+
return 'darwin';
|
|
66
|
+
case 'win32':
|
|
67
|
+
return 'windows';
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`Unsupported platform for Mihomo download: ${platform}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getMihomoArchTokens(arch = process.arch) {
|
|
73
|
+
switch (arch) {
|
|
74
|
+
case 'x64':
|
|
75
|
+
return ['amd64'];
|
|
76
|
+
case 'arm64':
|
|
77
|
+
return ['arm64-v8', 'arm64'];
|
|
78
|
+
case 'arm':
|
|
79
|
+
return ['armv7', 'arm'];
|
|
80
|
+
case 'ia32':
|
|
81
|
+
return ['386'];
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`Unsupported architecture for Mihomo download: ${arch}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function getMihomoCpuLevelTokens(platform = process.platform, arch = process.arch) {
|
|
87
|
+
if (platform !== 'linux' && platform !== 'darwin') {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
if (arch !== 'x64') {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return ['v3', 'v2', 'v1'];
|
|
94
|
+
}
|
|
95
|
+
function findMihomoAsset(assets, platform = process.platform, arch = process.arch) {
|
|
96
|
+
const platformToken = getMihomoPlatformToken(platform);
|
|
97
|
+
const archTokens = getMihomoArchTokens(arch);
|
|
98
|
+
const cpuLevelTokens = getMihomoCpuLevelTokens(platform, arch);
|
|
99
|
+
const extension = platform === 'win32' ? '.zip' : '.gz';
|
|
100
|
+
const candidates = assets.filter((asset) => asset.name.startsWith(`mihomo-${platformToken}-`) &&
|
|
101
|
+
asset.name.endsWith(extension));
|
|
102
|
+
const sortCandidates = (left, right) => {
|
|
103
|
+
const leftHasGoTag = left.name.includes('-go');
|
|
104
|
+
const rightHasGoTag = right.name.includes('-go');
|
|
105
|
+
if (leftHasGoTag !== rightHasGoTag) {
|
|
106
|
+
return leftHasGoTag ? 1 : -1;
|
|
107
|
+
}
|
|
108
|
+
const leftCompatible = left.name.includes('compatible');
|
|
109
|
+
const rightCompatible = right.name.includes('compatible');
|
|
110
|
+
if (leftCompatible !== rightCompatible) {
|
|
111
|
+
return leftCompatible ? 1 : -1;
|
|
112
|
+
}
|
|
113
|
+
return left.name.localeCompare(right.name);
|
|
114
|
+
};
|
|
115
|
+
for (const archToken of archTokens) {
|
|
116
|
+
const archMatches = candidates.filter((asset) => asset.name.includes(`-${archToken}-`));
|
|
117
|
+
if (archMatches.length === 0) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const cpuLevelToken of cpuLevelTokens) {
|
|
121
|
+
const cpuMatches = archMatches
|
|
122
|
+
.filter((asset) => asset.name.includes(`-${cpuLevelToken}-`))
|
|
123
|
+
.sort(sortCandidates);
|
|
124
|
+
if (cpuMatches.length > 0) {
|
|
125
|
+
return cpuMatches[0];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const fallbackMatches = archMatches.sort(sortCandidates);
|
|
129
|
+
if (fallbackMatches.length > 0) {
|
|
130
|
+
return fallbackMatches[0];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`No Mihomo binary found for platform ${platform} and architecture ${arch}.`);
|
|
134
|
+
}
|
|
135
|
+
function findFileRecursively(rootDir, matcher) {
|
|
136
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
const nested = findFileRecursively(fullPath, matcher);
|
|
141
|
+
if (nested)
|
|
142
|
+
return nested;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (matcher.test(entry.name)) {
|
|
146
|
+
return fullPath;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
async function fetchMihomoLatestRelease() {
|
|
152
|
+
const response = await fetch(MIHOMO_LATEST_RELEASE_API_URL, {
|
|
153
|
+
headers: {
|
|
154
|
+
Accept: 'application/vnd.github+json',
|
|
155
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`Failed to fetch Mihomo release metadata: ${response.status} ${response.statusText}`);
|
|
160
|
+
}
|
|
161
|
+
return (await response.json());
|
|
162
|
+
}
|
|
163
|
+
async function downloadMihomoBinary(targetDir) {
|
|
164
|
+
(0, utils_1.ensureDir)(targetDir);
|
|
165
|
+
const release = await fetchMihomoLatestRelease();
|
|
166
|
+
const asset = findMihomoAsset(release.assets);
|
|
167
|
+
const binaryPath = path.join(targetDir, getMihomoBinaryFilename());
|
|
168
|
+
if (process.platform === 'win32') {
|
|
169
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-mihomo-'));
|
|
170
|
+
try {
|
|
171
|
+
await (0, unzip_url_1.unzip)(asset.browser_download_url, tempDir);
|
|
172
|
+
const extractedBinary = findFileRecursively(tempDir, /^mihomo.*\.exe$/i);
|
|
173
|
+
if (!extractedBinary) {
|
|
174
|
+
throw new Error(`Mihomo executable was not found in asset ${asset.name}.`);
|
|
175
|
+
}
|
|
176
|
+
fs.copyFileSync(extractedBinary, binaryPath);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const response = await fetch(asset.browser_download_url, {
|
|
184
|
+
headers: {
|
|
185
|
+
Accept: 'application/octet-stream',
|
|
186
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Failed to download Mihomo binary: ${response.status} ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
const compressedBinary = Buffer.from(await response.arrayBuffer());
|
|
193
|
+
fs.writeFileSync(binaryPath, (0, zlib_1.gunzipSync)(compressedBinary));
|
|
194
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
binaryPath,
|
|
198
|
+
downloadUrl: asset.browser_download_url,
|
|
199
|
+
version: release.tag_name,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function writeMihomoConfig(targetPath) {
|
|
203
|
+
const configPath = targetPath || path.join(process.cwd(), 'config.yml');
|
|
204
|
+
(0, utils_1.ensureDir)(path.dirname(configPath));
|
|
205
|
+
fs.writeFileSync(configPath, MIHOMO_CONFIG_TEMPLATE);
|
|
206
|
+
return { configPath };
|
|
207
|
+
}
|
package/libs/index.ts
CHANGED
|
@@ -66,3 +66,6 @@ export { setupDevEnvironment } from './dev-setup'
|
|
|
66
66
|
|
|
67
67
|
// Re-export android-setup functionality
|
|
68
68
|
export { setupAndroidEnvironment } from './android-setup'
|
|
69
|
+
|
|
70
|
+
// Re-export mihomo functionality
|
|
71
|
+
export { writeMihomoConfig, downloadMihomoBinary } from './mihomo'
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { gzipSync } from 'zlib'
|
|
5
|
+
|
|
6
|
+
jest.mock('execa', () => ({
|
|
7
|
+
execa: jest.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
jest.mock('@gengjiawen/unzip-url', () => ({
|
|
11
|
+
unzip: jest.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
import { unzip } from '@gengjiawen/unzip-url'
|
|
15
|
+
import { downloadMihomoBinary, writeMihomoConfig } from './mihomo'
|
|
16
|
+
|
|
17
|
+
describe('writeMihomoConfig', () => {
|
|
18
|
+
let tempHome: string
|
|
19
|
+
let tempCwd: string
|
|
20
|
+
let homedirSpy: jest.SpiedFunction<typeof os.homedir>
|
|
21
|
+
let cwdSpy: jest.SpiedFunction<typeof process.cwd>
|
|
22
|
+
let originalPlatformDescriptor: PropertyDescriptor | undefined
|
|
23
|
+
let originalArchDescriptor: PropertyDescriptor | undefined
|
|
24
|
+
let originalFetch: typeof global.fetch | undefined
|
|
25
|
+
let originalAppData: string | undefined
|
|
26
|
+
const unzipMock = unzip as jest.MockedFunction<typeof unzip>
|
|
27
|
+
|
|
28
|
+
function setProcessRuntime(
|
|
29
|
+
platform: NodeJS.Platform,
|
|
30
|
+
arch: NodeJS.Architecture
|
|
31
|
+
): void {
|
|
32
|
+
Object.defineProperty(process, 'platform', {
|
|
33
|
+
configurable: true,
|
|
34
|
+
value: platform,
|
|
35
|
+
})
|
|
36
|
+
Object.defineProperty(process, 'arch', {
|
|
37
|
+
configurable: true,
|
|
38
|
+
value: arch,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-mihomo-'))
|
|
44
|
+
tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-mihomo-cwd-'))
|
|
45
|
+
homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
|
|
46
|
+
cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(tempCwd)
|
|
47
|
+
originalPlatformDescriptor = Object.getOwnPropertyDescriptor(
|
|
48
|
+
process,
|
|
49
|
+
'platform'
|
|
50
|
+
)
|
|
51
|
+
originalArchDescriptor = Object.getOwnPropertyDescriptor(process, 'arch')
|
|
52
|
+
originalFetch = global.fetch
|
|
53
|
+
originalAppData = process.env.APPDATA
|
|
54
|
+
process.env.APPDATA = path.join(tempHome, 'AppData', 'Roaming')
|
|
55
|
+
unzipMock.mockReset()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
homedirSpy.mockRestore()
|
|
60
|
+
cwdSpy.mockRestore()
|
|
61
|
+
if (originalAppData === undefined) {
|
|
62
|
+
delete process.env.APPDATA
|
|
63
|
+
} else {
|
|
64
|
+
process.env.APPDATA = originalAppData
|
|
65
|
+
}
|
|
66
|
+
if (originalPlatformDescriptor) {
|
|
67
|
+
Object.defineProperty(process, 'platform', originalPlatformDescriptor)
|
|
68
|
+
}
|
|
69
|
+
if (originalArchDescriptor) {
|
|
70
|
+
Object.defineProperty(process, 'arch', originalArchDescriptor)
|
|
71
|
+
}
|
|
72
|
+
if (originalFetch === undefined) {
|
|
73
|
+
global.fetch = undefined as unknown as typeof global.fetch
|
|
74
|
+
} else {
|
|
75
|
+
global.fetch = originalFetch
|
|
76
|
+
}
|
|
77
|
+
fs.rmSync(tempHome, { recursive: true, force: true })
|
|
78
|
+
fs.rmSync(tempCwd, { recursive: true, force: true })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('writes default mihomo config.yml to current directory', () => {
|
|
82
|
+
const result = writeMihomoConfig()
|
|
83
|
+
|
|
84
|
+
expect(result.configPath).toBe(path.join(tempCwd, 'config.yml'))
|
|
85
|
+
|
|
86
|
+
const content = fs.readFileSync(result.configPath, 'utf8')
|
|
87
|
+
expect(content).toContain('mixed-port: 7890')
|
|
88
|
+
expect(content).toContain('mode: rule')
|
|
89
|
+
expect(content).toContain('type: socks5')
|
|
90
|
+
expect(content).toContain('name: socks5-proxy')
|
|
91
|
+
expect(content).toContain('server: your.ip')
|
|
92
|
+
expect(content).toContain('port: 6153')
|
|
93
|
+
expect(content).toContain('proxy-groups:')
|
|
94
|
+
expect(content).toContain('- name: PROXY')
|
|
95
|
+
expect(content).toContain('- socks5-proxy')
|
|
96
|
+
expect(content).toContain('auto-detect-interface: true')
|
|
97
|
+
expect(content).toContain('- IP-CIDR,127.0.0.0/8,DIRECT')
|
|
98
|
+
expect(content).toContain('- IP-CIDR,10.0.0.0/8,DIRECT')
|
|
99
|
+
expect(content).toContain('- IP-CIDR,172.16.0.0/12,DIRECT')
|
|
100
|
+
expect(content).toContain('- IP-CIDR,192.168.0.0/16,DIRECT')
|
|
101
|
+
expect(content).toContain('- IP-CIDR,169.254.0.0/16,DIRECT')
|
|
102
|
+
expect(content).toContain('- IP-CIDR,100.64.0.0/10,DIRECT')
|
|
103
|
+
expect(content).toContain('- IP-CIDR6,fc00::/7,DIRECT')
|
|
104
|
+
expect(content).toContain('- IP-CIDR6,fe80::/10,DIRECT')
|
|
105
|
+
expect(content).toContain('- DOMAIN-SUFFIX,local,DIRECT')
|
|
106
|
+
expect(content).toContain('- DOMAIN-SUFFIX,cn,DIRECT')
|
|
107
|
+
expect(content).toContain('- GEOIP,CN,DIRECT')
|
|
108
|
+
expect(content).toContain('- MATCH,PROXY')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('writes mihomo config to custom target path', () => {
|
|
112
|
+
const customPath = path.join(tempHome, 'custom', 'mihomo.yml')
|
|
113
|
+
const result = writeMihomoConfig(customPath)
|
|
114
|
+
|
|
115
|
+
expect(result.configPath).toBe(customPath)
|
|
116
|
+
expect(fs.existsSync(customPath)).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('downloads linux mihomo binary and prefers v3 without go tag', async () => {
|
|
120
|
+
const targetDir = path.join(tempCwd, 'mihomo-bin')
|
|
121
|
+
const fetchMock = jest.fn()
|
|
122
|
+
const binaryContent = Buffer.from('#!/bin/sh\necho mihomo\n', 'utf8')
|
|
123
|
+
|
|
124
|
+
setProcessRuntime('linux', 'x64')
|
|
125
|
+
|
|
126
|
+
fetchMock
|
|
127
|
+
.mockResolvedValueOnce({
|
|
128
|
+
ok: true,
|
|
129
|
+
json: async () => ({
|
|
130
|
+
tag_name: 'v1.19.13',
|
|
131
|
+
assets: [
|
|
132
|
+
{
|
|
133
|
+
name: 'mihomo-linux-amd64-v1-v1.19.13.gz',
|
|
134
|
+
browser_download_url:
|
|
135
|
+
'https://example.com/mihomo-linux-amd64-v1-v1.19.13.gz',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'mihomo-linux-amd64-v2-v1.19.13.gz',
|
|
139
|
+
browser_download_url:
|
|
140
|
+
'https://example.com/mihomo-linux-amd64-v2-v1.19.13.gz',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'mihomo-linux-amd64-v3-v1.19.13.gz',
|
|
144
|
+
browser_download_url:
|
|
145
|
+
'https://example.com/mihomo-linux-amd64-v3-v1.19.13.gz',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'mihomo-linux-amd64-v3-go120-v1.19.13.gz',
|
|
149
|
+
browser_download_url:
|
|
150
|
+
'https://example.com/mihomo-linux-amd64-v3-go120-v1.19.13.gz',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'mihomo-linux-amd64-v3-go123-v1.19.13.gz',
|
|
154
|
+
browser_download_url:
|
|
155
|
+
'https://example.com/mihomo-linux-amd64-v3-go123-v1.19.13.gz',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'mihomo-linux-amd64-v1.19.13.gz',
|
|
159
|
+
browser_download_url:
|
|
160
|
+
'https://example.com/mihomo-linux-amd64-v1.19.13.gz',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
})
|
|
165
|
+
.mockResolvedValueOnce({
|
|
166
|
+
ok: true,
|
|
167
|
+
arrayBuffer: async () => gzipSync(binaryContent),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
global.fetch = fetchMock as unknown as typeof global.fetch
|
|
171
|
+
|
|
172
|
+
const result = await downloadMihomoBinary(targetDir)
|
|
173
|
+
|
|
174
|
+
expect(result.version).toBe('v1.19.13')
|
|
175
|
+
expect(result.binaryPath).toBe(path.join(targetDir, 'mihomo'))
|
|
176
|
+
expect(result.downloadUrl).toBe(
|
|
177
|
+
'https://example.com/mihomo-linux-amd64-v3-v1.19.13.gz'
|
|
178
|
+
)
|
|
179
|
+
expect(fs.existsSync(result.binaryPath)).toBe(true)
|
|
180
|
+
expect(fs.readFileSync(result.binaryPath)).toEqual(binaryContent)
|
|
181
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('downloads darwin mihomo binary and selects darwin asset', async () => {
|
|
185
|
+
const targetDir = path.join(tempCwd, 'mihomo-darwin-bin')
|
|
186
|
+
const fetchMock = jest.fn()
|
|
187
|
+
const binaryContent = Buffer.from('#!/bin/sh\necho mihomo-darwin\n', 'utf8')
|
|
188
|
+
|
|
189
|
+
setProcessRuntime('darwin', 'x64')
|
|
190
|
+
|
|
191
|
+
fetchMock
|
|
192
|
+
.mockResolvedValueOnce({
|
|
193
|
+
ok: true,
|
|
194
|
+
json: async () => ({
|
|
195
|
+
tag_name: 'v1.19.13',
|
|
196
|
+
assets: [
|
|
197
|
+
{
|
|
198
|
+
name: 'mihomo-linux-amd64-v3-v1.19.13.gz',
|
|
199
|
+
browser_download_url:
|
|
200
|
+
'https://example.com/mihomo-linux-amd64-v3-v1.19.13.gz',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'mihomo-darwin-amd64-v3-go123-v1.19.13.gz',
|
|
204
|
+
browser_download_url:
|
|
205
|
+
'https://example.com/mihomo-darwin-amd64-v3-go123-v1.19.13.gz',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'mihomo-darwin-amd64-v2-v1.19.13.gz',
|
|
209
|
+
browser_download_url:
|
|
210
|
+
'https://example.com/mihomo-darwin-amd64-v2-v1.19.13.gz',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'mihomo-darwin-amd64-v3-v1.19.13.gz',
|
|
214
|
+
browser_download_url:
|
|
215
|
+
'https://example.com/mihomo-darwin-amd64-v3-v1.19.13.gz',
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
}),
|
|
219
|
+
})
|
|
220
|
+
.mockResolvedValueOnce({
|
|
221
|
+
ok: true,
|
|
222
|
+
arrayBuffer: async () => gzipSync(binaryContent),
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
global.fetch = fetchMock as unknown as typeof global.fetch
|
|
226
|
+
|
|
227
|
+
const result = await downloadMihomoBinary(targetDir)
|
|
228
|
+
|
|
229
|
+
expect(result.version).toBe('v1.19.13')
|
|
230
|
+
expect(result.binaryPath).toBe(path.join(targetDir, 'mihomo'))
|
|
231
|
+
expect(result.downloadUrl).toBe(
|
|
232
|
+
'https://example.com/mihomo-darwin-amd64-v3-v1.19.13.gz'
|
|
233
|
+
)
|
|
234
|
+
expect(fs.existsSync(result.binaryPath)).toBe(true)
|
|
235
|
+
expect(fs.readFileSync(result.binaryPath)).toEqual(binaryContent)
|
|
236
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('downloads windows mihomo binary and extracts zip asset', async () => {
|
|
240
|
+
const targetDir = path.join(tempCwd, 'mihomo-windows-bin')
|
|
241
|
+
const fetchMock = jest.fn()
|
|
242
|
+
const binaryContent = Buffer.from('windows-mihomo', 'utf8')
|
|
243
|
+
|
|
244
|
+
setProcessRuntime('win32', 'x64')
|
|
245
|
+
|
|
246
|
+
fetchMock.mockResolvedValueOnce({
|
|
247
|
+
ok: true,
|
|
248
|
+
json: async () => ({
|
|
249
|
+
tag_name: 'v1.19.13',
|
|
250
|
+
assets: [
|
|
251
|
+
{
|
|
252
|
+
name: 'mihomo-linux-amd64-v3-v1.19.13.gz',
|
|
253
|
+
browser_download_url:
|
|
254
|
+
'https://example.com/mihomo-linux-amd64-v3-v1.19.13.gz',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'mihomo-windows-amd64-go123-v1.19.13.zip',
|
|
258
|
+
browser_download_url:
|
|
259
|
+
'https://example.com/mihomo-windows-amd64-go123-v1.19.13.zip',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'mihomo-windows-amd64-v1.19.13.zip',
|
|
263
|
+
browser_download_url:
|
|
264
|
+
'https://example.com/mihomo-windows-amd64-v1.19.13.zip',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
}),
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
unzipMock.mockImplementation(async (_url, destination) => {
|
|
271
|
+
const nestedDir = path.join(destination, 'nested')
|
|
272
|
+
fs.mkdirSync(nestedDir, { recursive: true })
|
|
273
|
+
fs.writeFileSync(path.join(nestedDir, 'mihomo.exe'), binaryContent)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
global.fetch = fetchMock as unknown as typeof global.fetch
|
|
277
|
+
|
|
278
|
+
const result = await downloadMihomoBinary(targetDir)
|
|
279
|
+
|
|
280
|
+
expect(result.version).toBe('v1.19.13')
|
|
281
|
+
expect(result.binaryPath).toBe(path.join(targetDir, 'mihomo.exe'))
|
|
282
|
+
expect(result.downloadUrl).toBe(
|
|
283
|
+
'https://example.com/mihomo-windows-amd64-v1.19.13.zip'
|
|
284
|
+
)
|
|
285
|
+
expect(fs.existsSync(result.binaryPath)).toBe(true)
|
|
286
|
+
expect(fs.readFileSync(result.binaryPath)).toEqual(binaryContent)
|
|
287
|
+
expect(unzipMock).toHaveBeenCalledWith(
|
|
288
|
+
'https://example.com/mihomo-windows-amd64-v1.19.13.zip',
|
|
289
|
+
expect.any(String)
|
|
290
|
+
)
|
|
291
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
292
|
+
})
|
|
293
|
+
})
|
package/libs/mihomo.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { gunzipSync } from 'zlib'
|
|
5
|
+
import { unzip } from '@gengjiawen/unzip-url'
|
|
6
|
+
import { ensureDir } from './utils'
|
|
7
|
+
|
|
8
|
+
const MIHOMO_CONFIG_TEMPLATE = `mixed-port: 7890
|
|
9
|
+
mode: rule
|
|
10
|
+
log-level: info
|
|
11
|
+
dns:
|
|
12
|
+
enable: true
|
|
13
|
+
ipv6: false
|
|
14
|
+
default-nameserver:
|
|
15
|
+
- 1.1.1.1
|
|
16
|
+
- 223.5.5.5
|
|
17
|
+
- 119.29.29.29
|
|
18
|
+
sniffer:
|
|
19
|
+
enable: true
|
|
20
|
+
sniffing:
|
|
21
|
+
- tls
|
|
22
|
+
- http
|
|
23
|
+
tun:
|
|
24
|
+
enable: true
|
|
25
|
+
stack: system
|
|
26
|
+
auto-route: true
|
|
27
|
+
auto-redirect: true
|
|
28
|
+
auto-detect-interface: true
|
|
29
|
+
proxies:
|
|
30
|
+
- name: socks5-proxy
|
|
31
|
+
type: socks5
|
|
32
|
+
server: your.ip
|
|
33
|
+
port: 6153
|
|
34
|
+
proxy-groups:
|
|
35
|
+
- name: PROXY
|
|
36
|
+
type: select
|
|
37
|
+
proxies:
|
|
38
|
+
- socks5-proxy
|
|
39
|
+
rules:
|
|
40
|
+
- IP-CIDR,127.0.0.0/8,DIRECT
|
|
41
|
+
- IP-CIDR,10.0.0.0/8,DIRECT
|
|
42
|
+
- IP-CIDR,172.16.0.0/12,DIRECT
|
|
43
|
+
- IP-CIDR,192.168.0.0/16,DIRECT
|
|
44
|
+
- IP-CIDR,169.254.0.0/16,DIRECT
|
|
45
|
+
- IP-CIDR,100.64.0.0/10,DIRECT
|
|
46
|
+
- IP-CIDR6,fc00::/7,DIRECT
|
|
47
|
+
- IP-CIDR6,fe80::/10,DIRECT
|
|
48
|
+
- DOMAIN-SUFFIX,local,DIRECT
|
|
49
|
+
- DOMAIN-SUFFIX,cn,DIRECT
|
|
50
|
+
- GEOIP,CN,DIRECT
|
|
51
|
+
- MATCH,PROXY
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const MIHOMO_LATEST_RELEASE_API_URL =
|
|
55
|
+
'https://api.github.com/repos/MetaCubeX/mihomo/releases/latest'
|
|
56
|
+
|
|
57
|
+
interface MihomoReleaseAsset {
|
|
58
|
+
name: string
|
|
59
|
+
browser_download_url: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface MihomoRelease {
|
|
63
|
+
tag_name: string
|
|
64
|
+
assets: MihomoReleaseAsset[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getMihomoBinaryFilename(platform = process.platform): string {
|
|
68
|
+
return platform === 'win32' ? 'mihomo.exe' : 'mihomo'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getMihomoPlatformToken(platform = process.platform): string {
|
|
72
|
+
switch (platform) {
|
|
73
|
+
case 'linux':
|
|
74
|
+
return 'linux'
|
|
75
|
+
case 'darwin':
|
|
76
|
+
return 'darwin'
|
|
77
|
+
case 'win32':
|
|
78
|
+
return 'windows'
|
|
79
|
+
default:
|
|
80
|
+
throw new Error(`Unsupported platform for Mihomo download: ${platform}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getMihomoArchTokens(arch = process.arch): string[] {
|
|
85
|
+
switch (arch) {
|
|
86
|
+
case 'x64':
|
|
87
|
+
return ['amd64']
|
|
88
|
+
case 'arm64':
|
|
89
|
+
return ['arm64-v8', 'arm64']
|
|
90
|
+
case 'arm':
|
|
91
|
+
return ['armv7', 'arm']
|
|
92
|
+
case 'ia32':
|
|
93
|
+
return ['386']
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unsupported architecture for Mihomo download: ${arch}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getMihomoCpuLevelTokens(
|
|
100
|
+
platform = process.platform,
|
|
101
|
+
arch = process.arch
|
|
102
|
+
): string[] {
|
|
103
|
+
if (platform !== 'linux' && platform !== 'darwin') {
|
|
104
|
+
return []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arch !== 'x64') {
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return ['v3', 'v2', 'v1']
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findMihomoAsset(
|
|
115
|
+
assets: MihomoReleaseAsset[],
|
|
116
|
+
platform = process.platform,
|
|
117
|
+
arch = process.arch
|
|
118
|
+
): MihomoReleaseAsset {
|
|
119
|
+
const platformToken = getMihomoPlatformToken(platform)
|
|
120
|
+
const archTokens = getMihomoArchTokens(arch)
|
|
121
|
+
const cpuLevelTokens = getMihomoCpuLevelTokens(platform, arch)
|
|
122
|
+
const extension = platform === 'win32' ? '.zip' : '.gz'
|
|
123
|
+
|
|
124
|
+
const candidates = assets.filter(
|
|
125
|
+
(asset) =>
|
|
126
|
+
asset.name.startsWith(`mihomo-${platformToken}-`) &&
|
|
127
|
+
asset.name.endsWith(extension)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const sortCandidates = (
|
|
131
|
+
left: MihomoReleaseAsset,
|
|
132
|
+
right: MihomoReleaseAsset
|
|
133
|
+
): number => {
|
|
134
|
+
const leftHasGoTag = left.name.includes('-go')
|
|
135
|
+
const rightHasGoTag = right.name.includes('-go')
|
|
136
|
+
|
|
137
|
+
if (leftHasGoTag !== rightHasGoTag) {
|
|
138
|
+
return leftHasGoTag ? 1 : -1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const leftCompatible = left.name.includes('compatible')
|
|
142
|
+
const rightCompatible = right.name.includes('compatible')
|
|
143
|
+
|
|
144
|
+
if (leftCompatible !== rightCompatible) {
|
|
145
|
+
return leftCompatible ? 1 : -1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return left.name.localeCompare(right.name)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const archToken of archTokens) {
|
|
152
|
+
const archMatches = candidates.filter((asset) =>
|
|
153
|
+
asset.name.includes(`-${archToken}-`)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if (archMatches.length === 0) {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const cpuLevelToken of cpuLevelTokens) {
|
|
161
|
+
const cpuMatches = archMatches
|
|
162
|
+
.filter((asset) => asset.name.includes(`-${cpuLevelToken}-`))
|
|
163
|
+
.sort(sortCandidates)
|
|
164
|
+
|
|
165
|
+
if (cpuMatches.length > 0) {
|
|
166
|
+
return cpuMatches[0]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const fallbackMatches = archMatches.sort(sortCandidates)
|
|
171
|
+
|
|
172
|
+
if (fallbackMatches.length > 0) {
|
|
173
|
+
return fallbackMatches[0]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new Error(
|
|
178
|
+
`No Mihomo binary found for platform ${platform} and architecture ${arch}.`
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findFileRecursively(rootDir: string, matcher: RegExp): string | null {
|
|
183
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true })
|
|
184
|
+
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const fullPath = path.join(rootDir, entry.name)
|
|
187
|
+
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
const nested = findFileRecursively(fullPath, matcher)
|
|
190
|
+
if (nested) return nested
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (matcher.test(entry.name)) {
|
|
195
|
+
return fullPath
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function fetchMihomoLatestRelease(): Promise<MihomoRelease> {
|
|
203
|
+
const response = await fetch(MIHOMO_LATEST_RELEASE_API_URL, {
|
|
204
|
+
headers: {
|
|
205
|
+
Accept: 'application/vnd.github+json',
|
|
206
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Failed to fetch Mihomo release metadata: ${response.status} ${response.statusText}`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (await response.json()) as MihomoRelease
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function downloadMihomoBinary(targetDir: string): Promise<{
|
|
220
|
+
binaryPath: string
|
|
221
|
+
downloadUrl: string
|
|
222
|
+
version: string
|
|
223
|
+
}> {
|
|
224
|
+
ensureDir(targetDir)
|
|
225
|
+
|
|
226
|
+
const release = await fetchMihomoLatestRelease()
|
|
227
|
+
const asset = findMihomoAsset(release.assets)
|
|
228
|
+
const binaryPath = path.join(targetDir, getMihomoBinaryFilename())
|
|
229
|
+
|
|
230
|
+
if (process.platform === 'win32') {
|
|
231
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-mihomo-'))
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await unzip(asset.browser_download_url, tempDir)
|
|
235
|
+
const extractedBinary = findFileRecursively(tempDir, /^mihomo.*\.exe$/i)
|
|
236
|
+
|
|
237
|
+
if (!extractedBinary) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Mihomo executable was not found in asset ${asset.name}.`
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.copyFileSync(extractedBinary, binaryPath)
|
|
244
|
+
} finally {
|
|
245
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
const response = await fetch(asset.browser_download_url, {
|
|
249
|
+
headers: {
|
|
250
|
+
Accept: 'application/octet-stream',
|
|
251
|
+
'User-Agent': '@gengjiawen/os-init',
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Failed to download Mihomo binary: ${response.status} ${response.statusText}`
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const compressedBinary = Buffer.from(await response.arrayBuffer())
|
|
262
|
+
fs.writeFileSync(binaryPath, gunzipSync(compressedBinary))
|
|
263
|
+
fs.chmodSync(binaryPath, 0o755)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
binaryPath,
|
|
268
|
+
downloadUrl: asset.browser_download_url,
|
|
269
|
+
version: release.tag_name,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Write Mihomo config.yml */
|
|
274
|
+
export function writeMihomoConfig(targetPath?: string): { configPath: string } {
|
|
275
|
+
const configPath = targetPath || path.join(process.cwd(), 'config.yml')
|
|
276
|
+
ensureDir(path.dirname(configPath))
|
|
277
|
+
fs.writeFileSync(configPath, MIHOMO_CONFIG_TEMPLATE)
|
|
278
|
+
|
|
279
|
+
return { configPath }
|
|
280
|
+
}
|