@jonathanleelx/meddle 0.4.0-beta.9 → 0.4.1-beta.1
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/README.md +1 -1
- package/bin/commands/update.js +38 -1
- package/bin/lib/update-check.js +69 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ irm https://raw.githubusercontent.com/JonathanLee-LX/meddle/main/scripts/install
|
|
|
34
34
|
| Windows x64 | `meddle-windows-x64.exe` |
|
|
35
35
|
| Windows ARM64 | `meddle-windows-arm64.exe` |
|
|
36
36
|
|
|
37
|
-
二进制产物随 [GitHub Release](https://github.com/JonathanLee-LX/meddle/releases) 发布,也可手动下载。安装目录默认 `~/.meddle/bin
|
|
37
|
+
二进制产物随 [GitHub Release](https://github.com/JonathanLee-LX/meddle/releases) 发布,也可手动下载。安装目录默认 `~/.local/bin`(已有 `~/.meddle/bin` 旧安装时原地升级),可通过 `MEDDLE_BIN_DIR` 环境变量自定义。`meddle update` 始终更新到当前运行二进制所在目录。
|
|
38
38
|
|
|
39
39
|
### 命令
|
|
40
40
|
|
package/bin/commands/update.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
getAutoUpdate,
|
|
26
26
|
setAutoUpdate,
|
|
27
27
|
inferChannel,
|
|
28
|
+
resolveUpdateBinDir,
|
|
28
29
|
} = require('../lib/update-check')
|
|
29
30
|
|
|
30
31
|
const PACKAGE_NAME = '@jonathanleelx/meddle'
|
|
@@ -41,6 +42,27 @@ function isNpmInstall() {
|
|
|
41
42
|
return installMethod === 'npm'
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function formatBytes(bytes) {
|
|
46
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
47
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
48
|
+
return `${bytes} B`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PROGRESS_WIDTH = 40
|
|
52
|
+
|
|
53
|
+
function drawProgressBar({ received, total, attempt, maxAttempts }) {
|
|
54
|
+
const label = attempt > 1 ? ` (第 ${attempt}/${maxAttempts} 次尝试)` : ''
|
|
55
|
+
if (total > 0) {
|
|
56
|
+
const ratio = Math.min(1, received / total)
|
|
57
|
+
const filled = Math.round(PROGRESS_WIDTH * ratio)
|
|
58
|
+
const bar = '#'.repeat(filled) + '-'.repeat(PROGRESS_WIDTH - filled)
|
|
59
|
+
const line = ` [${bar}] ${(ratio * 100).toFixed(1)}% ${formatBytes(received)}/${formatBytes(total)}${label}`
|
|
60
|
+
process.stdout.write(`\r${line}`)
|
|
61
|
+
} else {
|
|
62
|
+
process.stdout.write(`\r 下载中 ${formatBytes(received)}${label}...`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
function showHelp() {
|
|
45
67
|
output.plain(`
|
|
46
68
|
meddle update - 检查并升级 meddle
|
|
@@ -82,20 +104,35 @@ async function resolveLatest({ force = false, notify = true, channel = 'stable',
|
|
|
82
104
|
|
|
83
105
|
async function installVersion(version) {
|
|
84
106
|
if (!isNpmInstall()) {
|
|
85
|
-
const binDir =
|
|
107
|
+
const binDir = resolveUpdateBinDir({ home })
|
|
86
108
|
const destFile = path.join(binDir, os.platform() === 'win32' ? 'meddle.exe' : 'meddle')
|
|
87
109
|
output.info(`下载 meddle ${version} (${os.platform()}/${os.arch()})...`)
|
|
110
|
+
let progressShown = false
|
|
111
|
+
const showProgress = output.isJsonMode() ? () => {} : drawProgressBar
|
|
88
112
|
try {
|
|
89
113
|
const result = await downloadBinaryAsset({
|
|
90
114
|
version,
|
|
91
115
|
destFile,
|
|
92
116
|
platform: os.platform(),
|
|
93
117
|
arch: os.arch(),
|
|
118
|
+
onAttempt: (attempt, maxAttempts) => {
|
|
119
|
+
if (attempt > 1) {
|
|
120
|
+
process.stdout.write('\n')
|
|
121
|
+
progressShown = false
|
|
122
|
+
output.info(`连接中断,正在重试 (${attempt}/${maxAttempts})...`)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
onProgress: (p) => {
|
|
126
|
+
showProgress(p)
|
|
127
|
+
progressShown = true
|
|
128
|
+
},
|
|
94
129
|
})
|
|
130
|
+
if (progressShown) process.stdout.write('\n')
|
|
95
131
|
output.success(`已安装 meddle ${version} 到 ${result.installed}`)
|
|
96
132
|
if (result.backup) output.info(`旧版本已备份到 ${result.backup}`)
|
|
97
133
|
output.info('重启 meddle 后新版本生效')
|
|
98
134
|
} catch (err) {
|
|
135
|
+
if (progressShown) process.stdout.write('\n')
|
|
99
136
|
output.error(`升级失败: ${err && err.message ? err.message : err}`)
|
|
100
137
|
process.exit(1)
|
|
101
138
|
}
|
package/bin/lib/update-check.js
CHANGED
|
@@ -280,10 +280,13 @@ function sha256Hex(buffer) {
|
|
|
280
280
|
* thrown (Windows commonly fails with EPERM on a running binary).
|
|
281
281
|
*
|
|
282
282
|
* The payload download is retried (default 3 attempts) because binaries are
|
|
283
|
-
* large (100MB+) and flaky connections drop mid-body.
|
|
283
|
+
* large (100MB+) and flaky connections drop mid-body. Progress is reported
|
|
284
|
+
* via `onProgress` while streaming (received/total bytes), and `onAttempt`
|
|
285
|
+
* fires before each attempt (skipping retries is visible to callers).
|
|
284
286
|
* @param {{ version: string, destFile: string, platform: string, arch: string,
|
|
285
287
|
* baseUrl?: string, fetchImpl?: Function, timeoutMs?: number,
|
|
286
288
|
* retries?: number, retryDelayMs?: number,
|
|
289
|
+
* onProgress?: Function, onAttempt?: Function,
|
|
287
290
|
* fsImpl?: object }} opts
|
|
288
291
|
* @returns {Promise<{ installed: string, backup: string, version: string }>}
|
|
289
292
|
*/
|
|
@@ -295,25 +298,63 @@ async function downloadBinaryAsset(opts) {
|
|
|
295
298
|
const payloadTimeout = opts.timeoutMs || DEFAULT_DOWNLOAD_TIMEOUT_MS
|
|
296
299
|
const maxRetries = opts.retries === undefined ? 3 : opts.retries
|
|
297
300
|
const retryDelayMs = opts.retryDelayMs === undefined ? 1000 : opts.retryDelayMs
|
|
301
|
+
const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : () => {}
|
|
302
|
+
const onAttempt = typeof opts.onAttempt === 'function' ? opts.onAttempt : () => {}
|
|
298
303
|
|
|
299
304
|
const asset = getAssetName({ platform, arch })
|
|
300
305
|
const assetUrl = `${baseUrl}/v${version}/${asset}`
|
|
301
306
|
const shaUrl = `${assetUrl}.sha256`
|
|
302
307
|
|
|
303
|
-
// The binary is large (100MB+); the checksum sidecar is tiny. Use
|
|
304
|
-
// timeout for the payload
|
|
308
|
+
// The binary is large (100MB+); the checksum sidecar is tiny. Use an IDLE
|
|
309
|
+
// timeout for the payload (reset on every received chunk — a slow-but-steady
|
|
310
|
+
// transfer is never killed by a total wall-time budget) and the regular
|
|
311
|
+
// timeout for the metadata fetch.
|
|
305
312
|
const fetchPayload = async () => {
|
|
306
313
|
let lastError = null
|
|
307
|
-
for (let attempt =
|
|
308
|
-
if (attempt >
|
|
309
|
-
await new Promise((r) => setTimeout(r, retryDelayMs * attempt))
|
|
314
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
315
|
+
if (attempt > 1 && retryDelayMs > 0) {
|
|
316
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * (attempt - 1)))
|
|
310
317
|
}
|
|
318
|
+
onAttempt(attempt, maxRetries + 1)
|
|
319
|
+
let idleTimer = null
|
|
320
|
+
const controller = new AbortController()
|
|
321
|
+
const resetIdle = () => {
|
|
322
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
323
|
+
idleTimer = setTimeout(() => {
|
|
324
|
+
controller.abort(new Error('download stalled (idle timeout)'))
|
|
325
|
+
}, payloadTimeout)
|
|
326
|
+
if (typeof idleTimer.unref === 'function') idleTimer.unref()
|
|
327
|
+
}
|
|
328
|
+
resetIdle()
|
|
311
329
|
try {
|
|
312
|
-
const response = await doFetch(assetUrl, { signal:
|
|
330
|
+
const response = await doFetch(assetUrl, { signal: controller.signal })
|
|
313
331
|
if (!response.ok) throw new Error(`download failed (${response.status})`)
|
|
314
|
-
|
|
332
|
+
const total = Number(response.headers.get('content-length')) || 0
|
|
333
|
+
const chunks = []
|
|
334
|
+
let received = 0
|
|
335
|
+
if (response.body && typeof response.body.getReader === 'function') {
|
|
336
|
+
const reader = response.body.getReader()
|
|
337
|
+
for (;;) {
|
|
338
|
+
resetIdle()
|
|
339
|
+
const { done, value } = await reader.read()
|
|
340
|
+
if (done) break
|
|
341
|
+
chunks.push(Buffer.from(value))
|
|
342
|
+
received += value.byteLength
|
|
343
|
+
onProgress({ received, total })
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
resetIdle()
|
|
347
|
+
const buf = Buffer.from(await response.arrayBuffer())
|
|
348
|
+
chunks.push(buf)
|
|
349
|
+
received = buf.length
|
|
350
|
+
onProgress({ received, total })
|
|
351
|
+
}
|
|
352
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
353
|
+
return Buffer.concat(chunks)
|
|
315
354
|
} catch (err) {
|
|
355
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
316
356
|
lastError = err
|
|
357
|
+
if (attempt > maxRetries) break
|
|
317
358
|
}
|
|
318
359
|
}
|
|
319
360
|
throw lastError || new Error(`download failed for ${assetUrl}`)
|
|
@@ -408,6 +449,24 @@ function printUpdateNotice(info) {
|
|
|
408
449
|
)
|
|
409
450
|
}
|
|
410
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Resolve where `meddle update` should install the new binary.
|
|
454
|
+
* 1. MEDDLE_BIN_DIR env (explicit override)
|
|
455
|
+
* 2. the directory of the RUNNING binary — in-place updates, so installs on
|
|
456
|
+
* legacy (~/.meddle/bin) or custom paths stay where they are
|
|
457
|
+
* 3. <home>/bin fallback (npm installs / no running binary)
|
|
458
|
+
* @param {{ home: string, execPath?: string, platform?: string }} opts
|
|
459
|
+
* @returns {string}
|
|
460
|
+
*/
|
|
461
|
+
function resolveUpdateBinDir({ home, execPath = process.execPath, platform = os.platform() } = {}) {
|
|
462
|
+
if (process.env.MEDDLE_BIN_DIR) return process.env.MEDDLE_BIN_DIR
|
|
463
|
+
const p = platform === 'win32' ? path.win32 : path
|
|
464
|
+
const exeName = p.basename(execPath).toLowerCase()
|
|
465
|
+
const expected = platform === 'win32' ? 'meddle.exe' : 'meddle'
|
|
466
|
+
if (exeName === expected) return p.dirname(execPath)
|
|
467
|
+
return p.join(home, 'bin')
|
|
468
|
+
}
|
|
469
|
+
|
|
411
470
|
/**
|
|
412
471
|
* Fire-and-forget startup check. Never blocks, never throws, never keeps the
|
|
413
472
|
* process alive. When auto-update is enabled (binary installs) the new binary
|
|
@@ -430,7 +489,7 @@ function isSameBinary(a, b) {
|
|
|
430
489
|
function runAsyncUpdateCheck(opts) {
|
|
431
490
|
const home = opts.home || resolveMeddleHome()
|
|
432
491
|
const installMethod = opts.installMethod || getInstallMethod({ moduleDir: __dirname })
|
|
433
|
-
const binDir = opts.binDir ||
|
|
492
|
+
const binDir = opts.binDir || resolveUpdateBinDir({ home, execPath: opts.execPath || process.execPath })
|
|
434
493
|
const delayMs = opts.delayMs === undefined ? 1500 : opts.delayMs
|
|
435
494
|
const execPath = opts.execPath || process.execPath
|
|
436
495
|
const exeBase = path.basename(execPath).toLowerCase()
|
|
@@ -493,6 +552,7 @@ module.exports = {
|
|
|
493
552
|
downloadBinaryAsset,
|
|
494
553
|
getAutoUpdate,
|
|
495
554
|
setAutoUpdate,
|
|
555
|
+
resolveUpdateBinDir,
|
|
496
556
|
runAsyncUpdateCheck,
|
|
497
557
|
DEFAULT_DOWNLOAD_TIMEOUT_MS,
|
|
498
558
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonathanleelx/meddle",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1-beta.1",
|
|
4
4
|
"description": "A simple development proxy server with custom routing rules, mock responses, HTTPS interception, and a plugin system",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|