@jonathanleelx/meddle 0.4.0-beta.7 → 0.4.0-beta.9

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.
@@ -47,8 +47,8 @@ meddle update - 检查并升级 meddle
47
47
 
48
48
  用法:
49
49
  meddle update 检查并升级(beta 版本默认走 beta 频道)
50
- meddle update --beta 强制使用 beta 测试频道
51
- meddle update --stable 强制使用稳定频道
50
+ meddle update --beta 切换到 beta 频道并升级到最新 beta 版本
51
+ meddle update --stable 切换到稳定频道并升级到最新稳定版本(可降级)
52
52
  meddle update --check 仅检查,提示可用版本 (有新版本时退出码为 2)
53
53
  meddle update --check --beta 仅检查 beta 版本
54
54
  meddle update --version <x.y.z> 安装指定版本
@@ -58,13 +58,14 @@ meddle update - 检查并升级 meddle
58
58
  `)
59
59
  }
60
60
 
61
- async function resolveLatest({ force = false, notify = true, channel = 'stable' } = {}) {
61
+ async function resolveLatest({ force = false, notify = true, channel = 'stable', explicitChannel = false } = {}) {
62
62
  const info = await checkForUpdate({
63
63
  home,
64
64
  installMethod,
65
65
  current,
66
66
  force,
67
67
  channel,
68
+ explicitChannel,
68
69
  })
69
70
  if (notify) {
70
71
  if (info.outdated) {
@@ -123,6 +124,7 @@ async function run() {
123
124
  const flag = args.find((a) => a === '--check' || a === 'status' || a === '--help' || a === '-h')
124
125
  const versionFlagIndex = args.indexOf('--version')
125
126
  const autoFlagIndex = args.indexOf('--auto')
127
+ const explicitChannel = args.includes('--beta') || args.includes('--stable')
126
128
  const channel = args.includes('--beta') ? 'beta' : args.includes('--stable') ? 'stable' : inferChannel(current)
127
129
 
128
130
  if (flag === '--help' || flag === '-h') {
@@ -131,7 +133,7 @@ async function run() {
131
133
  }
132
134
 
133
135
  if (flag === 'status') {
134
- const info = await resolveLatest({ notify: false, channel })
136
+ const info = await resolveLatest({ notify: false, channel, explicitChannel })
135
137
  output.header('Meddle Update Status')
136
138
  output.kv('Install method', installMethod)
137
139
  output.kv('Channel', channel)
@@ -181,13 +183,13 @@ async function run() {
181
183
  }
182
184
 
183
185
  if (flag === '--check') {
184
- const info = await resolveLatest({ channel })
186
+ const info = await resolveLatest({ channel, explicitChannel })
185
187
  if (info.outdated) process.exit(2)
186
188
  return
187
189
  }
188
190
 
189
191
  // default: check + upgrade
190
- const info = await resolveLatest({ channel })
192
+ const info = await resolveLatest({ channel, explicitChannel })
191
193
  if (!info.outdated) return
192
194
  await installVersion(info.latest)
193
195
  }
@@ -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,
@@ -265,8 +278,12 @@ function sha256Hex(buffer) {
265
278
  * the existing binary. The previous binary is kept as `<destFile>.bak`. When
266
279
  * the final replace fails the backup is restored and a descriptive error is
267
280
  * thrown (Windows commonly fails with EPERM on a running binary).
281
+ *
282
+ * The payload download is retried (default 3 attempts) because binaries are
283
+ * large (100MB+) and flaky connections drop mid-body.
268
284
  * @param {{ version: string, destFile: string, platform: string, arch: string,
269
285
  * baseUrl?: string, fetchImpl?: Function, timeoutMs?: number,
286
+ * retries?: number, retryDelayMs?: number,
270
287
  * fsImpl?: object }} opts
271
288
  * @returns {Promise<{ installed: string, backup: string, version: string }>}
272
289
  */
@@ -276,6 +293,8 @@ async function downloadBinaryAsset(opts) {
276
293
  const doFetch = opts.fetchImpl || fetch
277
294
  const fsImpl = opts.fsImpl || fs
278
295
  const payloadTimeout = opts.timeoutMs || DEFAULT_DOWNLOAD_TIMEOUT_MS
296
+ const maxRetries = opts.retries === undefined ? 3 : opts.retries
297
+ const retryDelayMs = opts.retryDelayMs === undefined ? 1000 : opts.retryDelayMs
279
298
 
280
299
  const asset = getAssetName({ platform, arch })
281
300
  const assetUrl = `${baseUrl}/v${version}/${asset}`
@@ -283,14 +302,27 @@ async function downloadBinaryAsset(opts) {
283
302
 
284
303
  // The binary is large (100MB+); the checksum sidecar is tiny. Use a long
285
304
  // timeout for the payload and the regular timeout for the metadata fetch.
286
- const [binaryResponse, shaResponse] = await Promise.all([
287
- doFetch(assetUrl, { signal: AbortSignal.timeout(payloadTimeout) }),
288
- doFetch(shaUrl, { signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) }),
289
- ])
290
- if (!binaryResponse.ok) throw new Error(`download failed (${binaryResponse.status})`)
305
+ const fetchPayload = async () => {
306
+ let lastError = null
307
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
308
+ if (attempt > 0 && retryDelayMs > 0) {
309
+ await new Promise((r) => setTimeout(r, retryDelayMs * attempt))
310
+ }
311
+ try {
312
+ const response = await doFetch(assetUrl, { signal: AbortSignal.timeout(payloadTimeout) })
313
+ if (!response.ok) throw new Error(`download failed (${response.status})`)
314
+ return Buffer.from(await response.arrayBuffer())
315
+ } catch (err) {
316
+ lastError = err
317
+ }
318
+ }
319
+ throw lastError || new Error(`download failed for ${assetUrl}`)
320
+ }
321
+
322
+ const shaResponse = await doFetch(shaUrl, { signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) })
291
323
  if (!shaResponse.ok) throw new Error(`checksum sidecar missing (${shaResponse.status})`)
292
324
 
293
- const payload = Buffer.from(await binaryResponse.arrayBuffer())
325
+ const payload = await fetchPayload()
294
326
  const shaText = await shaResponse.text()
295
327
  const expected = shaText.trim().split(/\s+/)[0]
296
328
  if (!/^[0-9a-f]{64}$/i.test(expected)) throw new Error(`invalid checksum sidecar`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonathanleelx/meddle",
3
- "version": "0.4.0-beta.7",
3
+ "version": "0.4.0-beta.9",
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",