@jonathanleelx/meddle 0.4.0-beta.8 → 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 +44 -6
- package/bin/lib/update-check.js +47 -9
- package/package.json +1 -1
package/bin/commands/update.js
CHANGED
|
@@ -41,14 +41,35 @@ 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
|
|
47
68
|
|
|
48
69
|
用法:
|
|
49
70
|
meddle update 检查并升级(beta 版本默认走 beta 频道)
|
|
50
|
-
meddle update --beta
|
|
51
|
-
meddle update --stable
|
|
71
|
+
meddle update --beta 切换到 beta 频道并升级到最新 beta 版本
|
|
72
|
+
meddle update --stable 切换到稳定频道并升级到最新稳定版本(可降级)
|
|
52
73
|
meddle update --check 仅检查,提示可用版本 (有新版本时退出码为 2)
|
|
53
74
|
meddle update --check --beta 仅检查 beta 版本
|
|
54
75
|
meddle update --version <x.y.z> 安装指定版本
|
|
@@ -58,13 +79,14 @@ meddle update - 检查并升级 meddle
|
|
|
58
79
|
`)
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
async function resolveLatest({ force = false, notify = true, channel = 'stable' } = {}) {
|
|
82
|
+
async function resolveLatest({ force = false, notify = true, channel = 'stable', explicitChannel = false } = {}) {
|
|
62
83
|
const info = await checkForUpdate({
|
|
63
84
|
home,
|
|
64
85
|
installMethod,
|
|
65
86
|
current,
|
|
66
87
|
force,
|
|
67
88
|
channel,
|
|
89
|
+
explicitChannel,
|
|
68
90
|
})
|
|
69
91
|
if (notify) {
|
|
70
92
|
if (info.outdated) {
|
|
@@ -84,17 +106,32 @@ async function installVersion(version) {
|
|
|
84
106
|
const binDir = process.env.MEDDLE_BIN_DIR || path.join(home, 'bin')
|
|
85
107
|
const destFile = path.join(binDir, os.platform() === 'win32' ? 'meddle.exe' : 'meddle')
|
|
86
108
|
output.info(`下载 meddle ${version} (${os.platform()}/${os.arch()})...`)
|
|
109
|
+
let progressShown = false
|
|
110
|
+
const showProgress = output.isJsonMode() ? () => {} : drawProgressBar
|
|
87
111
|
try {
|
|
88
112
|
const result = await downloadBinaryAsset({
|
|
89
113
|
version,
|
|
90
114
|
destFile,
|
|
91
115
|
platform: os.platform(),
|
|
92
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
|
+
},
|
|
93
128
|
})
|
|
129
|
+
if (progressShown) process.stdout.write('\n')
|
|
94
130
|
output.success(`已安装 meddle ${version} 到 ${result.installed}`)
|
|
95
131
|
if (result.backup) output.info(`旧版本已备份到 ${result.backup}`)
|
|
96
132
|
output.info('重启 meddle 后新版本生效')
|
|
97
133
|
} catch (err) {
|
|
134
|
+
if (progressShown) process.stdout.write('\n')
|
|
98
135
|
output.error(`升级失败: ${err && err.message ? err.message : err}`)
|
|
99
136
|
process.exit(1)
|
|
100
137
|
}
|
|
@@ -123,6 +160,7 @@ async function run() {
|
|
|
123
160
|
const flag = args.find((a) => a === '--check' || a === 'status' || a === '--help' || a === '-h')
|
|
124
161
|
const versionFlagIndex = args.indexOf('--version')
|
|
125
162
|
const autoFlagIndex = args.indexOf('--auto')
|
|
163
|
+
const explicitChannel = args.includes('--beta') || args.includes('--stable')
|
|
126
164
|
const channel = args.includes('--beta') ? 'beta' : args.includes('--stable') ? 'stable' : inferChannel(current)
|
|
127
165
|
|
|
128
166
|
if (flag === '--help' || flag === '-h') {
|
|
@@ -131,7 +169,7 @@ async function run() {
|
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
if (flag === 'status') {
|
|
134
|
-
const info = await resolveLatest({ notify: false, channel })
|
|
172
|
+
const info = await resolveLatest({ notify: false, channel, explicitChannel })
|
|
135
173
|
output.header('Meddle Update Status')
|
|
136
174
|
output.kv('Install method', installMethod)
|
|
137
175
|
output.kv('Channel', channel)
|
|
@@ -181,13 +219,13 @@ async function run() {
|
|
|
181
219
|
}
|
|
182
220
|
|
|
183
221
|
if (flag === '--check') {
|
|
184
|
-
const info = await resolveLatest({ channel })
|
|
222
|
+
const info = await resolveLatest({ channel, explicitChannel })
|
|
185
223
|
if (info.outdated) process.exit(2)
|
|
186
224
|
return
|
|
187
225
|
}
|
|
188
226
|
|
|
189
227
|
// default: check + upgrade
|
|
190
|
-
const info = await resolveLatest({ channel })
|
|
228
|
+
const info = await resolveLatest({ channel, explicitChannel })
|
|
191
229
|
if (!info.outdated) return
|
|
192
230
|
await installVersion(info.latest)
|
|
193
231
|
}
|
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
|
|
|
@@ -165,11 +165,23 @@ function inferChannel(current) {
|
|
|
165
165
|
return isValidVersion(current) && parseVersion(current).prerelease.length > 0 ? 'beta' : 'stable'
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Decide whether the install should move to `latest`. Explicit channel flags
|
|
170
|
+
* (--beta/--stable) mean "switch channels", so a different channel always
|
|
171
|
+
* triggers the move (even a downgrade); the inferred channel only upgrades.
|
|
172
|
+
* @param {{ latest: string, current: string, channel: string, explicitChannel: boolean }} opts
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
function outdatedByChannel({ latest, current, channel, explicitChannel }) {
|
|
176
|
+
if (explicitChannel && channel !== inferChannel(current)) return true
|
|
177
|
+
return compareVersions(latest, current) > 0
|
|
178
|
+
}
|
|
179
|
+
|
|
168
180
|
/**
|
|
169
181
|
* @param {{ home: string, fetchImpl?: Function, installMethod: string, current: string,
|
|
170
182
|
* now?: number, ttlMs?: number, force?: boolean,
|
|
171
183
|
* registryUrl?: string, latestUrl?: string, timeoutMs?: number,
|
|
172
|
-
* channel?: string }} opts
|
|
184
|
+
* channel?: string, explicitChannel?: boolean }} opts
|
|
173
185
|
* @returns {Promise<{ current: string, latest: string, outdated: boolean, fromCache: boolean, checkedAt: number, channel: string }>}
|
|
174
186
|
*/
|
|
175
187
|
async function checkForUpdate(opts) {
|
|
@@ -185,6 +197,7 @@ async function checkForUpdate(opts) {
|
|
|
185
197
|
latestUrl,
|
|
186
198
|
timeoutMs,
|
|
187
199
|
channel = inferChannel(current),
|
|
200
|
+
explicitChannel = false,
|
|
188
201
|
} = opts
|
|
189
202
|
|
|
190
203
|
const cacheFile = updateCachePath(home)
|
|
@@ -199,7 +212,7 @@ async function checkForUpdate(opts) {
|
|
|
199
212
|
return {
|
|
200
213
|
current,
|
|
201
214
|
latest: cached.version,
|
|
202
|
-
outdated:
|
|
215
|
+
outdated: outdatedByChannel({ latest: cached.version, current, channel, explicitChannel }),
|
|
203
216
|
fromCache: true,
|
|
204
217
|
checkedAt: cached.checkedAt,
|
|
205
218
|
channel,
|
|
@@ -226,7 +239,7 @@ async function checkForUpdate(opts) {
|
|
|
226
239
|
return {
|
|
227
240
|
current,
|
|
228
241
|
latest,
|
|
229
|
-
outdated:
|
|
242
|
+
outdated: outdatedByChannel({ latest, current, channel, explicitChannel }),
|
|
230
243
|
fromCache: false,
|
|
231
244
|
checkedAt,
|
|
232
245
|
channel,
|
|
@@ -267,10 +280,13 @@ function sha256Hex(buffer) {
|
|
|
267
280
|
* thrown (Windows commonly fails with EPERM on a running binary).
|
|
268
281
|
*
|
|
269
282
|
* The payload download is retried (default 3 attempts) because binaries are
|
|
270
|
-
* 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).
|
|
271
286
|
* @param {{ version: string, destFile: string, platform: string, arch: string,
|
|
272
287
|
* baseUrl?: string, fetchImpl?: Function, timeoutMs?: number,
|
|
273
288
|
* retries?: number, retryDelayMs?: number,
|
|
289
|
+
* onProgress?: Function, onAttempt?: Function,
|
|
274
290
|
* fsImpl?: object }} opts
|
|
275
291
|
* @returns {Promise<{ installed: string, backup: string, version: string }>}
|
|
276
292
|
*/
|
|
@@ -282,6 +298,8 @@ async function downloadBinaryAsset(opts) {
|
|
|
282
298
|
const payloadTimeout = opts.timeoutMs || DEFAULT_DOWNLOAD_TIMEOUT_MS
|
|
283
299
|
const maxRetries = opts.retries === undefined ? 3 : opts.retries
|
|
284
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 : () => {}
|
|
285
303
|
|
|
286
304
|
const asset = getAssetName({ platform, arch })
|
|
287
305
|
const assetUrl = `${baseUrl}/v${version}/${asset}`
|
|
@@ -291,16 +309,36 @@ async function downloadBinaryAsset(opts) {
|
|
|
291
309
|
// timeout for the payload and the regular timeout for the metadata fetch.
|
|
292
310
|
const fetchPayload = async () => {
|
|
293
311
|
let lastError = null
|
|
294
|
-
for (let attempt =
|
|
295
|
-
if (attempt >
|
|
296
|
-
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)))
|
|
297
315
|
}
|
|
316
|
+
onAttempt(attempt, maxRetries + 1)
|
|
298
317
|
try {
|
|
299
318
|
const response = await doFetch(assetUrl, { signal: AbortSignal.timeout(payloadTimeout) })
|
|
300
319
|
if (!response.ok) throw new Error(`download failed (${response.status})`)
|
|
301
|
-
|
|
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)
|
|
302
339
|
} catch (err) {
|
|
303
340
|
lastError = err
|
|
341
|
+
if (attempt > maxRetries) break
|
|
304
342
|
}
|
|
305
343
|
}
|
|
306
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",
|