@jonathanleelx/meddle 0.4.0 → 0.4.1-beta.2

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 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`,可通过 `MEDDLE_BIN_DIR` 环境变量自定义。
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
 
@@ -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'
@@ -103,7 +104,7 @@ async function resolveLatest({ force = false, notify = true, channel = 'stable',
103
104
 
104
105
  async function installVersion(version) {
105
106
  if (!isNpmInstall()) {
106
- const binDir = process.env.MEDDLE_BIN_DIR || path.join(home, 'bin')
107
+ const binDir = resolveUpdateBinDir({ home })
107
108
  const destFile = path.join(binDir, os.platform() === 'win32' ? 'meddle.exe' : 'meddle')
108
109
  output.info(`下载 meddle ${version} (${os.platform()}/${os.arch()})...`)
109
110
  let progressShown = false
@@ -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 = 5 * 60 * 1000
28
+ const DEFAULT_DOWNLOAD_TIMEOUT_MS = 120 * 1000
29
29
 
30
30
  const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/
31
31
 
@@ -305,8 +305,10 @@ async function downloadBinaryAsset(opts) {
305
305
  const assetUrl = `${baseUrl}/v${version}/${asset}`
306
306
  const shaUrl = `${assetUrl}.sha256`
307
307
 
308
- // The binary is large (100MB+); the checksum sidecar is tiny. Use a long
309
- // timeout for the payload and the regular timeout for the metadata fetch.
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.
310
312
  const fetchPayload = async () => {
311
313
  let lastError = null
312
314
  for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
@@ -314,8 +316,18 @@ async function downloadBinaryAsset(opts) {
314
316
  await new Promise((r) => setTimeout(r, retryDelayMs * (attempt - 1)))
315
317
  }
316
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()
317
329
  try {
318
- const response = await doFetch(assetUrl, { signal: AbortSignal.timeout(payloadTimeout) })
330
+ const response = await doFetch(assetUrl, { signal: controller.signal })
319
331
  if (!response.ok) throw new Error(`download failed (${response.status})`)
320
332
  const total = Number(response.headers.get('content-length')) || 0
321
333
  const chunks = []
@@ -323,6 +335,7 @@ async function downloadBinaryAsset(opts) {
323
335
  if (response.body && typeof response.body.getReader === 'function') {
324
336
  const reader = response.body.getReader()
325
337
  for (;;) {
338
+ resetIdle()
326
339
  const { done, value } = await reader.read()
327
340
  if (done) break
328
341
  chunks.push(Buffer.from(value))
@@ -330,13 +343,16 @@ async function downloadBinaryAsset(opts) {
330
343
  onProgress({ received, total })
331
344
  }
332
345
  } else {
346
+ resetIdle()
333
347
  const buf = Buffer.from(await response.arrayBuffer())
334
348
  chunks.push(buf)
335
349
  received = buf.length
336
350
  onProgress({ received, total })
337
351
  }
352
+ if (idleTimer) clearTimeout(idleTimer)
338
353
  return Buffer.concat(chunks)
339
354
  } catch (err) {
355
+ if (idleTimer) clearTimeout(idleTimer)
340
356
  lastError = err
341
357
  if (attempt > maxRetries) break
342
358
  }
@@ -344,11 +360,16 @@ async function downloadBinaryAsset(opts) {
344
360
  throw lastError || new Error(`download failed for ${assetUrl}`)
345
361
  }
346
362
 
363
+ // Fetch the tiny checksum sidecar and CONSUME its body right away. The
364
+ // sidecar timeout must not span the (potentially multi-minute) payload
365
+ // download — otherwise the sidecar body stream aborts before it is read
366
+ // and the whole install fails with "operation aborted" right after the
367
+ // payload reached 100%.
347
368
  const shaResponse = await doFetch(shaUrl, { signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) })
348
369
  if (!shaResponse.ok) throw new Error(`checksum sidecar missing (${shaResponse.status})`)
370
+ const shaText = await shaResponse.text()
349
371
 
350
372
  const payload = await fetchPayload()
351
- const shaText = await shaResponse.text()
352
373
  const expected = shaText.trim().split(/\s+/)[0]
353
374
  if (!/^[0-9a-f]{64}$/i.test(expected)) throw new Error(`invalid checksum sidecar`)
354
375
  if (sha256Hex(payload) !== expected.toLowerCase()) {
@@ -433,6 +454,24 @@ function printUpdateNotice(info) {
433
454
  )
434
455
  }
435
456
 
457
+ /**
458
+ * Resolve where `meddle update` should install the new binary.
459
+ * 1. MEDDLE_BIN_DIR env (explicit override)
460
+ * 2. the directory of the RUNNING binary — in-place updates, so installs on
461
+ * legacy (~/.meddle/bin) or custom paths stay where they are
462
+ * 3. <home>/bin fallback (npm installs / no running binary)
463
+ * @param {{ home: string, execPath?: string, platform?: string }} opts
464
+ * @returns {string}
465
+ */
466
+ function resolveUpdateBinDir({ home, execPath = process.execPath, platform = os.platform() } = {}) {
467
+ if (process.env.MEDDLE_BIN_DIR) return process.env.MEDDLE_BIN_DIR
468
+ const p = platform === 'win32' ? path.win32 : path
469
+ const exeName = p.basename(execPath).toLowerCase()
470
+ const expected = platform === 'win32' ? 'meddle.exe' : 'meddle'
471
+ if (exeName === expected) return p.dirname(execPath)
472
+ return p.join(home, 'bin')
473
+ }
474
+
436
475
  /**
437
476
  * Fire-and-forget startup check. Never blocks, never throws, never keeps the
438
477
  * process alive. When auto-update is enabled (binary installs) the new binary
@@ -455,7 +494,7 @@ function isSameBinary(a, b) {
455
494
  function runAsyncUpdateCheck(opts) {
456
495
  const home = opts.home || resolveMeddleHome()
457
496
  const installMethod = opts.installMethod || getInstallMethod({ moduleDir: __dirname })
458
- const binDir = opts.binDir || process.env.MEDDLE_BIN_DIR || path.join(home, 'bin')
497
+ const binDir = opts.binDir || resolveUpdateBinDir({ home, execPath: opts.execPath || process.execPath })
459
498
  const delayMs = opts.delayMs === undefined ? 1500 : opts.delayMs
460
499
  const execPath = opts.execPath || process.execPath
461
500
  const exeBase = path.basename(execPath).toLowerCase()
@@ -518,6 +557,7 @@ module.exports = {
518
557
  downloadBinaryAsset,
519
558
  getAutoUpdate,
520
559
  setAutoUpdate,
560
+ resolveUpdateBinDir,
521
561
  runAsyncUpdateCheck,
522
562
  DEFAULT_DOWNLOAD_TIMEOUT_MS,
523
563
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonathanleelx/meddle",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-beta.2",
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",