@jonathanleelx/meddle 0.4.0-beta.9 → 0.4.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/bin/commands/update.js +36 -0
- package/bin/lib/update-check.js +31 -6
- package/package.json +1 -1
package/bin/commands/update.js
CHANGED
|
@@ -41,6 +41,27 @@ function isNpmInstall() {
|
|
|
41
41
|
return installMethod === 'npm'
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function formatBytes(bytes) {
|
|
45
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
46
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
47
|
+
return `${bytes} B`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PROGRESS_WIDTH = 40
|
|
51
|
+
|
|
52
|
+
function drawProgressBar({ received, total, attempt, maxAttempts }) {
|
|
53
|
+
const label = attempt > 1 ? ` (第 ${attempt}/${maxAttempts} 次尝试)` : ''
|
|
54
|
+
if (total > 0) {
|
|
55
|
+
const ratio = Math.min(1, received / total)
|
|
56
|
+
const filled = Math.round(PROGRESS_WIDTH * ratio)
|
|
57
|
+
const bar = '#'.repeat(filled) + '-'.repeat(PROGRESS_WIDTH - filled)
|
|
58
|
+
const line = ` [${bar}] ${(ratio * 100).toFixed(1)}% ${formatBytes(received)}/${formatBytes(total)}${label}`
|
|
59
|
+
process.stdout.write(`\r${line}`)
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write(`\r 下载中 ${formatBytes(received)}${label}...`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
function showHelp() {
|
|
45
66
|
output.plain(`
|
|
46
67
|
meddle update - 检查并升级 meddle
|
|
@@ -85,17 +106,32 @@ async function installVersion(version) {
|
|
|
85
106
|
const binDir = process.env.MEDDLE_BIN_DIR || path.join(home, 'bin')
|
|
86
107
|
const destFile = path.join(binDir, os.platform() === 'win32' ? 'meddle.exe' : 'meddle')
|
|
87
108
|
output.info(`下载 meddle ${version} (${os.platform()}/${os.arch()})...`)
|
|
109
|
+
let progressShown = false
|
|
110
|
+
const showProgress = output.isJsonMode() ? () => {} : drawProgressBar
|
|
88
111
|
try {
|
|
89
112
|
const result = await downloadBinaryAsset({
|
|
90
113
|
version,
|
|
91
114
|
destFile,
|
|
92
115
|
platform: os.platform(),
|
|
93
116
|
arch: os.arch(),
|
|
117
|
+
onAttempt: (attempt, maxAttempts) => {
|
|
118
|
+
if (attempt > 1) {
|
|
119
|
+
process.stdout.write('\n')
|
|
120
|
+
progressShown = false
|
|
121
|
+
output.info(`连接中断,正在重试 (${attempt}/${maxAttempts})...`)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
onProgress: (p) => {
|
|
125
|
+
showProgress(p)
|
|
126
|
+
progressShown = true
|
|
127
|
+
},
|
|
94
128
|
})
|
|
129
|
+
if (progressShown) process.stdout.write('\n')
|
|
95
130
|
output.success(`已安装 meddle ${version} 到 ${result.installed}`)
|
|
96
131
|
if (result.backup) output.info(`旧版本已备份到 ${result.backup}`)
|
|
97
132
|
output.info('重启 meddle 后新版本生效')
|
|
98
133
|
} catch (err) {
|
|
134
|
+
if (progressShown) process.stdout.write('\n')
|
|
99
135
|
output.error(`升级失败: ${err && err.message ? err.message : err}`)
|
|
100
136
|
process.exit(1)
|
|
101
137
|
}
|
package/bin/lib/update-check.js
CHANGED
|
@@ -25,7 +25,7 @@ const DEFAULT_GITHUB_LATEST_URL = `https://github.com/${REPO}/releases/latest`
|
|
|
25
25
|
const DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${REPO}/releases/download`
|
|
26
26
|
const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
|
27
27
|
const DEFAULT_TIMEOUT_MS = 5000
|
|
28
|
-
const DEFAULT_DOWNLOAD_TIMEOUT_MS =
|
|
28
|
+
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000
|
|
29
29
|
|
|
30
30
|
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/
|
|
31
31
|
|
|
@@ -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,6 +298,8 @@ 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}`
|
|
@@ -304,16 +309,36 @@ async function downloadBinaryAsset(opts) {
|
|
|
304
309
|
// timeout for the payload and the regular timeout for the metadata fetch.
|
|
305
310
|
const fetchPayload = async () => {
|
|
306
311
|
let lastError = null
|
|
307
|
-
for (let attempt =
|
|
308
|
-
if (attempt >
|
|
309
|
-
await new Promise((r) => setTimeout(r, retryDelayMs * attempt))
|
|
312
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
313
|
+
if (attempt > 1 && retryDelayMs > 0) {
|
|
314
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * (attempt - 1)))
|
|
310
315
|
}
|
|
316
|
+
onAttempt(attempt, maxRetries + 1)
|
|
311
317
|
try {
|
|
312
318
|
const response = await doFetch(assetUrl, { signal: AbortSignal.timeout(payloadTimeout) })
|
|
313
319
|
if (!response.ok) throw new Error(`download failed (${response.status})`)
|
|
314
|
-
|
|
320
|
+
const total = Number(response.headers.get('content-length')) || 0
|
|
321
|
+
const chunks = []
|
|
322
|
+
let received = 0
|
|
323
|
+
if (response.body && typeof response.body.getReader === 'function') {
|
|
324
|
+
const reader = response.body.getReader()
|
|
325
|
+
for (;;) {
|
|
326
|
+
const { done, value } = await reader.read()
|
|
327
|
+
if (done) break
|
|
328
|
+
chunks.push(Buffer.from(value))
|
|
329
|
+
received += value.byteLength
|
|
330
|
+
onProgress({ received, total })
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
const buf = Buffer.from(await response.arrayBuffer())
|
|
334
|
+
chunks.push(buf)
|
|
335
|
+
received = buf.length
|
|
336
|
+
onProgress({ received, total })
|
|
337
|
+
}
|
|
338
|
+
return Buffer.concat(chunks)
|
|
315
339
|
} catch (err) {
|
|
316
340
|
lastError = err
|
|
341
|
+
if (attempt > maxRetries) break
|
|
317
342
|
}
|
|
318
343
|
}
|
|
319
344
|
throw lastError || new Error(`download failed for ${assetUrl}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonathanleelx/meddle",
|
|
3
|
-
"version": "0.4.0
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|