@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.
@@ -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 强制使用 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
  }
@@ -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 = 120 * 1000
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: compareVersions(cached.version, current) > 0,
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: compareVersions(latest, current) > 0,
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 = 0; attempt <= maxRetries; attempt++) {
295
- if (attempt > 0 && retryDelayMs > 0) {
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
- return Buffer.from(await response.arrayBuffer())
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-beta.8",
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",