moondesk 0.11.0 → 0.11.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.
@@ -103,7 +103,67 @@ function cleanupOldBinaryVersions(options = {}) {
103
103
  return { removed, skipped };
104
104
  }
105
105
 
106
- async function fetchRequired(fetchImpl, url, maxBytes, timeoutMs = METADATA_TIMEOUT_MS) {
106
+ function reportDownloadProgress(callback, downloadedBytes, totalBytes) {
107
+ if (typeof callback !== "function") return;
108
+ try {
109
+ callback({ downloadedBytes, totalBytes });
110
+ } catch {
111
+ // Download reporting is best-effort and must never make a verified install fail.
112
+ }
113
+ }
114
+
115
+ async function readResponseBuffer(response, url, maxBytes, onProgress) {
116
+ const declaredLength = Number(response.headers.get("content-length"));
117
+ const totalBytes = Number.isFinite(declaredLength) && declaredLength >= 0 ? declaredLength : null;
118
+ if (totalBytes !== null && totalBytes > maxBytes) {
119
+ throw new Error(`${url} is unexpectedly large (${totalBytes} bytes)`);
120
+ }
121
+
122
+ reportDownloadProgress(onProgress, 0, totalBytes);
123
+
124
+ if (!response.body || typeof response.body.getReader !== "function") {
125
+ const buffer = Buffer.from(await response.arrayBuffer());
126
+ if (buffer.length > maxBytes) {
127
+ throw new Error(`${url} exceeded the ${maxBytes}-byte download limit`);
128
+ }
129
+ reportDownloadProgress(onProgress, buffer.length, totalBytes);
130
+ return buffer;
131
+ }
132
+
133
+ const reader = response.body.getReader();
134
+ const chunks = [];
135
+ let downloadedBytes = 0;
136
+ try {
137
+ while (true) {
138
+ const { done, value } = await reader.read();
139
+ if (done) break;
140
+ const chunk = Buffer.from(value);
141
+ downloadedBytes += chunk.length;
142
+ if (downloadedBytes > maxBytes) {
143
+ try {
144
+ await reader.cancel();
145
+ } catch {
146
+ // Preserve the size-limit error below if cancellation itself fails.
147
+ }
148
+ throw new Error(`${url} exceeded the ${maxBytes}-byte download limit`);
149
+ }
150
+ chunks.push(chunk);
151
+ reportDownloadProgress(onProgress, downloadedBytes, totalBytes);
152
+ }
153
+ } finally {
154
+ reader.releaseLock?.();
155
+ }
156
+
157
+ return Buffer.concat(chunks, downloadedBytes);
158
+ }
159
+
160
+ async function fetchRequired(
161
+ fetchImpl,
162
+ url,
163
+ maxBytes,
164
+ timeoutMs = METADATA_TIMEOUT_MS,
165
+ onProgress,
166
+ ) {
107
167
  const response = await fetchImpl(url, {
108
168
  headers: {
109
169
  "User-Agent": `moondesk-npm/${version}`,
@@ -115,17 +175,7 @@ async function fetchRequired(fetchImpl, url, maxBytes, timeoutMs = METADATA_TIME
115
175
  throw new Error(`${url} returned HTTP ${response.status}`);
116
176
  }
117
177
 
118
- const contentLength = Number(response.headers.get("content-length"));
119
- if (Number.isFinite(contentLength) && contentLength > maxBytes) {
120
- throw new Error(`${url} is unexpectedly large (${contentLength} bytes)`);
121
- }
122
-
123
- const buffer = Buffer.from(await response.arrayBuffer());
124
- if (buffer.length > maxBytes) {
125
- throw new Error(`${url} exceeded the ${maxBytes}-byte download limit`);
126
- }
127
-
128
- return buffer;
178
+ return readResponseBuffer(response, url, maxBytes, onProgress);
129
179
  }
130
180
 
131
181
  function expectedSha256(checksums, name) {
@@ -343,6 +393,7 @@ async function ensureBinary(options = {}) {
343
393
  `${releaseBaseUrl}/${targetInfo.assetName}`,
344
394
  MAX_BINARY_BYTES,
345
395
  BINARY_TIMEOUT_MS,
396
+ options.onDownloadProgress,
346
397
  );
347
398
  const actual = sha256Buffer(binary);
348
399
 
@@ -381,8 +432,54 @@ async function ensureBinary(options = {}) {
381
432
  }
382
433
  }
383
434
 
435
+ function formatMiB(bytes) {
436
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
437
+ }
438
+
439
+ function createDownloadProgressReporter(logger = console) {
440
+ let started = false;
441
+ let lastPercentBucket = 0;
442
+ let lastUnknownBytes = 0;
443
+ const unknownReportStep = 5 * 1024 * 1024;
444
+
445
+ return ({ downloadedBytes, totalBytes }) => {
446
+ if (!started) {
447
+ started = true;
448
+ if (Number.isFinite(totalBytes) && totalBytes > 0) {
449
+ logger.log?.(
450
+ `Downloading MoonDesk ${version} native binary (${formatMiB(totalBytes)})...`,
451
+ );
452
+ } else {
453
+ logger.log?.(`Downloading MoonDesk ${version} native binary...`);
454
+ }
455
+ }
456
+
457
+ if (!Number.isFinite(downloadedBytes) || downloadedBytes <= 0) return;
458
+
459
+ if (Number.isFinite(totalBytes) && totalBytes > 0) {
460
+ const percent = Math.min(100, Math.floor((downloadedBytes / totalBytes) * 100));
461
+ const bucket = percent === 100 ? 100 : Math.floor(percent / 25) * 25;
462
+ if (bucket >= 25 && bucket > lastPercentBucket) {
463
+ lastPercentBucket = bucket;
464
+ logger.log?.(
465
+ `MoonDesk ${version} native binary download: ${bucket}% (${formatMiB(Math.min(downloadedBytes, totalBytes))} / ${formatMiB(totalBytes)})`,
466
+ );
467
+ }
468
+ return;
469
+ }
470
+
471
+ if (downloadedBytes - lastUnknownBytes >= unknownReportStep) {
472
+ lastUnknownBytes = downloadedBytes;
473
+ logger.log?.(
474
+ `MoonDesk ${version} native binary download: ${formatMiB(downloadedBytes)} downloaded...`,
475
+ );
476
+ }
477
+ };
478
+ }
479
+
384
480
  module.exports = {
385
481
  cleanupOldBinaryVersions,
482
+ createDownloadProgressReporter,
386
483
  ensureBinary,
387
484
  expectedSha256,
388
485
  resolveTarget,
@@ -390,7 +487,7 @@ module.exports = {
390
487
  };
391
488
 
392
489
  if (require.main === module) {
393
- ensureBinary()
490
+ ensureBinary({ onDownloadProgress: createDownloadProgressReporter(console) })
394
491
  .then((binaryPath) => {
395
492
  console.log(`MoonDesk native binary ready at ${binaryPath}`);
396
493
  })
package/npm/moondesk.js CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  const fs = require("node:fs");
4
4
  const { spawn } = require("node:child_process");
5
- const { cleanupOldBinaryVersions, ensureBinary } = require("./install-binary");
5
+ const {
6
+ cleanupOldBinaryVersions,
7
+ createDownloadProgressReporter,
8
+ ensureBinary,
9
+ } = require("./install-binary");
6
10
  const {
7
11
  UPDATE_EXIT_CODE,
8
12
  acquireUpdateLock,
@@ -176,7 +180,9 @@ async function orchestrate(options = {}) {
176
180
 
177
181
  let binaryPath;
178
182
  try {
179
- binaryPath = await ensureBinaryImpl();
183
+ binaryPath = await ensureBinaryImpl({
184
+ onDownloadProgress: createDownloadProgressReporter(logger),
185
+ });
180
186
  } catch (error) {
181
187
  stopUpdateMonitor();
182
188
  cleanupEphemeralUpdateFiles(updateStatePath, updateRequestPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moondesk",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "MoonDesk — use ChatGPT Chat as a local coding agent, by Shattermoon.",
5
5
  "author": "Shattermoon",
6
6
  "license": "MIT",