moondesk 0.8.0 → 0.9.1
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 +1 -1
- package/npm/moondesk.js +45 -7
- package/npm/update-manager.js +266 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npm install -g moondesk
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
> [!IMPORTANT]
|
|
12
|
-
> MoonDesk runs tools on your computer
|
|
12
|
+
> MoonDesk runs tools locally on your computer. Review commands before running them, and use an isolated environment for untrusted projects or code.
|
|
13
13
|
|
|
14
14
|
## Why MoonDesk?
|
|
15
15
|
|
package/npm/moondesk.js
CHANGED
|
@@ -7,15 +7,18 @@ const {
|
|
|
7
7
|
UPDATE_EXIT_CODE,
|
|
8
8
|
acquireUpdateLock,
|
|
9
9
|
cleanupOldUpdateVersions,
|
|
10
|
+
changelogNoticePath,
|
|
10
11
|
compareStableVersions,
|
|
11
12
|
createUpdateRequestPath,
|
|
12
13
|
createUpdateStatePath,
|
|
13
14
|
installExactVersion,
|
|
14
15
|
installedWrapperVersion,
|
|
15
16
|
readUpdateRequest,
|
|
17
|
+
refreshUpdateRequestToLatest,
|
|
16
18
|
restartUpdatedWrapper,
|
|
17
19
|
startUpdateMonitor,
|
|
18
20
|
verifyInstalledWrapperVersion,
|
|
21
|
+
writePostUpdateNotice,
|
|
19
22
|
} = require("./update-manager");
|
|
20
23
|
|
|
21
24
|
function cleanManagedUpdateEnv(source = process.env) {
|
|
@@ -23,6 +26,7 @@ function cleanManagedUpdateEnv(source = process.env) {
|
|
|
23
26
|
delete env.MOONDESK_NPM_MANAGED;
|
|
24
27
|
delete env.MOONDESK_UPDATE_REQUEST_PATH;
|
|
25
28
|
delete env.MOONDESK_UPDATE_STATE_PATH;
|
|
29
|
+
delete env.MOONDESK_CHANGELOG_NOTICE_PATH;
|
|
26
30
|
return env;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -132,11 +136,14 @@ async function orchestrate(options = {}) {
|
|
|
132
136
|
const startUpdateMonitorImpl = options.startUpdateMonitorImpl ?? startUpdateMonitor;
|
|
133
137
|
const runNativeImpl = options.runNativeImpl ?? runNative;
|
|
134
138
|
const readUpdateRequestImpl = options.readUpdateRequestImpl ?? readUpdateRequest;
|
|
139
|
+
const refreshUpdateRequestToLatestImpl =
|
|
140
|
+
options.refreshUpdateRequestToLatestImpl ?? refreshUpdateRequestToLatest;
|
|
135
141
|
const acquireUpdateLockImpl = options.acquireUpdateLockImpl ?? acquireUpdateLock;
|
|
136
142
|
const installedWrapperVersionImpl = options.installedWrapperVersionImpl ?? installedWrapperVersion;
|
|
137
143
|
const installExactVersionImpl = options.installExactVersionImpl ?? installExactVersion;
|
|
138
144
|
const verifyInstalledWrapperVersionImpl =
|
|
139
145
|
options.verifyInstalledWrapperVersionImpl ?? verifyInstalledWrapperVersion;
|
|
146
|
+
const writePostUpdateNoticeImpl = options.writePostUpdateNoticeImpl ?? writePostUpdateNotice;
|
|
140
147
|
const restartUpdatedWrapperImpl = options.restartUpdatedWrapperImpl ?? restartUpdatedWrapper;
|
|
141
148
|
const wrapperPath = options.wrapperPath ?? __filename;
|
|
142
149
|
|
|
@@ -180,6 +187,11 @@ async function orchestrate(options = {}) {
|
|
|
180
187
|
MOONDESK_UPDATE_REQUEST_PATH: updateRequestPath,
|
|
181
188
|
MOONDESK_UPDATE_STATE_PATH: updateStatePath,
|
|
182
189
|
});
|
|
190
|
+
try {
|
|
191
|
+
childEnv.MOONDESK_CHANGELOG_NOTICE_PATH = changelogNoticePath();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.warn?.(`MoonDesk could not prepare its optional changelog notice path: ${error.message}`);
|
|
194
|
+
}
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
let result;
|
|
@@ -227,7 +239,7 @@ async function orchestrate(options = {}) {
|
|
|
227
239
|
return { code: 1, signal: null };
|
|
228
240
|
}
|
|
229
241
|
|
|
230
|
-
|
|
242
|
+
let effectiveRequest = request;
|
|
231
243
|
let releaseUpdateLock = null;
|
|
232
244
|
let restartVersion = request.targetVersion;
|
|
233
245
|
try {
|
|
@@ -236,26 +248,44 @@ async function orchestrate(options = {}) {
|
|
|
236
248
|
env: baseEnv,
|
|
237
249
|
});
|
|
238
250
|
|
|
251
|
+
try {
|
|
252
|
+
const refreshedRequest = await refreshUpdateRequestToLatestImpl(effectiveRequest);
|
|
253
|
+
if (compareStableVersions(refreshedRequest.targetVersion, effectiveRequest.targetVersion) > 0) {
|
|
254
|
+
logger.log(
|
|
255
|
+
`MoonDesk ${refreshedRequest.targetVersion} became available while this update was pending; updating directly to the newest version.`,
|
|
256
|
+
);
|
|
257
|
+
effectiveRequest = refreshedRequest;
|
|
258
|
+
restartVersion = effectiveRequest.targetVersion;
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logger.warn?.(
|
|
262
|
+
`MoonDesk could not refresh the latest version before updating; continuing with ${effectiveRequest.targetVersion}: ${error.message}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
logger.log(
|
|
267
|
+
`Updating MoonDesk ${effectiveRequest.currentVersion} -> ${effectiveRequest.targetVersion}...`,
|
|
268
|
+
);
|
|
239
269
|
const alreadyInstalled = installedWrapperVersionImpl();
|
|
240
|
-
const comparison = compareStableVersions(alreadyInstalled,
|
|
270
|
+
const comparison = compareStableVersions(alreadyInstalled, effectiveRequest.targetVersion);
|
|
241
271
|
if (comparison < 0) {
|
|
242
|
-
await installExactVersionImpl(
|
|
272
|
+
await installExactVersionImpl(effectiveRequest.targetVersion, {
|
|
243
273
|
cwd: originalCwd,
|
|
244
274
|
env: baseEnv,
|
|
245
275
|
});
|
|
246
|
-
verifyInstalledWrapperVersionImpl(
|
|
276
|
+
verifyInstalledWrapperVersionImpl(effectiveRequest.targetVersion);
|
|
247
277
|
} else if (comparison === 0) {
|
|
248
|
-
logger.log(`MoonDesk ${
|
|
278
|
+
logger.log(`MoonDesk ${effectiveRequest.targetVersion} was already installed by another process.`);
|
|
249
279
|
} else {
|
|
250
280
|
restartVersion = alreadyInstalled;
|
|
251
281
|
logger.log(
|
|
252
|
-
`MoonDesk ${alreadyInstalled} is already installed, so the updater will not downgrade it to ${
|
|
282
|
+
`MoonDesk ${alreadyInstalled} is already installed, so the updater will not downgrade it to ${effectiveRequest.targetVersion}.`,
|
|
253
283
|
);
|
|
254
284
|
}
|
|
255
285
|
} catch (error) {
|
|
256
286
|
logger.error(`MoonDesk update failed: ${error.message}`);
|
|
257
287
|
logger.error(
|
|
258
|
-
`Run 'npm install -g moondesk@${
|
|
288
|
+
`Run 'npm install -g moondesk@${effectiveRequest.targetVersion}' manually to retry this exact version.`,
|
|
259
289
|
);
|
|
260
290
|
return { code: 1, signal: null };
|
|
261
291
|
} finally {
|
|
@@ -266,6 +296,14 @@ async function orchestrate(options = {}) {
|
|
|
266
296
|
}
|
|
267
297
|
}
|
|
268
298
|
|
|
299
|
+
if (restartVersion === effectiveRequest.targetVersion) {
|
|
300
|
+
try {
|
|
301
|
+
writePostUpdateNoticeImpl(effectiveRequest, restartVersion);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
logger.warn?.(`MoonDesk updated, but could not persist its one-time changelog notice: ${error.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
269
307
|
logger.log(`MoonDesk ${restartVersion} is installed. Restarting...`);
|
|
270
308
|
try {
|
|
271
309
|
return await restartUpdatedWrapperImpl(wrapperPath, originalArgs, {
|
package/npm/update-manager.js
CHANGED
|
@@ -14,16 +14,34 @@ const currentVersion = packageJson.version;
|
|
|
14
14
|
const UPDATE_EXIT_CODE = 75;
|
|
15
15
|
const UPDATE_STATE_SCHEMA_VERSION = 1;
|
|
16
16
|
const UPDATE_REQUEST_SCHEMA_VERSION = 1;
|
|
17
|
+
const CHANGELOG_NOTICE_SCHEMA_VERSION = 1;
|
|
17
18
|
const REGISTRY_LATEST_URL = "https://registry.npmjs.org/moondesk/latest";
|
|
19
|
+
const GITHUB_REPOSITORY = "Shattermoon/moondesk";
|
|
20
|
+
const GITHUB_RELEASES_API_URL = `https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=20`;
|
|
21
|
+
const GITHUB_RELEASE_TAG_API_BASE = `https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags`;
|
|
22
|
+
const GITHUB_RELEASE_WEB_BASE = `https://github.com/${GITHUB_REPOSITORY}/releases/tag`;
|
|
23
|
+
const GITHUB_PULL_WEB_BASE = `https://github.com/${GITHUB_REPOSITORY}/pull`;
|
|
18
24
|
const UPDATE_CHECK_INTERVAL_MS = 15 * 60_000;
|
|
19
25
|
const UPDATE_CHECK_TIMEOUT_MS = 15_000;
|
|
20
26
|
const MAX_UPDATE_METADATA_BYTES = 64 * 1024;
|
|
27
|
+
const MAX_CHANGELOG_METADATA_BYTES = 512 * 1024;
|
|
28
|
+
const MAX_CHANGELOG_ITEMS = 12;
|
|
29
|
+
const MAX_CHANGELOG_ITEM_CHARS = 180;
|
|
21
30
|
const MAX_UPDATE_REQUEST_BYTES = 16 * 1024;
|
|
22
31
|
const MAX_NPM_ROOT_BYTES = 16 * 1024;
|
|
23
32
|
const NPM_ROOT_TIMEOUT_MS = 10_000;
|
|
24
33
|
const UPDATE_LOCK_WAIT_MS = 60_000;
|
|
25
34
|
const UPDATE_LOCK_POLL_MS = 200;
|
|
26
35
|
|
|
36
|
+
function escapeRegExp(value) {
|
|
37
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const GITHUB_PULL_ATTRIBUTION_RE = new RegExp(
|
|
41
|
+
`\\s+by\\s+@[A-Za-z0-9_-]+\\s+in\\s+${escapeRegExp(GITHUB_PULL_WEB_BASE)}\\/\\d+\\s*$`,
|
|
42
|
+
"i",
|
|
43
|
+
);
|
|
44
|
+
|
|
27
45
|
function parseStableVersion(input) {
|
|
28
46
|
if (typeof input !== "string") {
|
|
29
47
|
return null;
|
|
@@ -101,6 +119,49 @@ function cleanupOldUpdateVersions(options = {}) {
|
|
|
101
119
|
return { removed, skipped };
|
|
102
120
|
}
|
|
103
121
|
|
|
122
|
+
function changelogNoticePath(version = currentVersion) {
|
|
123
|
+
if (!parseStableVersion(version)) {
|
|
124
|
+
throw new Error(`MoonDesk changelog version must be a stable semantic version: ${version}`);
|
|
125
|
+
}
|
|
126
|
+
return path.join(updateRootDir(), `v${version}`, "post-update.json");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizePersistedReleaseNotes(value) {
|
|
130
|
+
if (!Array.isArray(value)) return [];
|
|
131
|
+
const notes = [];
|
|
132
|
+
for (const item of value) {
|
|
133
|
+
if (typeof item !== "string") continue;
|
|
134
|
+
const trimmed = item.trim();
|
|
135
|
+
if (!trimmed || trimmed.length > MAX_CHANGELOG_ITEM_CHARS) continue;
|
|
136
|
+
notes.push(trimmed);
|
|
137
|
+
if (notes.length >= MAX_CHANGELOG_ITEMS) break;
|
|
138
|
+
}
|
|
139
|
+
return notes;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeReleaseUrl(value, version) {
|
|
143
|
+
const expected = `${GITHUB_RELEASE_WEB_BASE}/v${version}`;
|
|
144
|
+
return typeof value === "string" && value === expected ? value : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writePostUpdateNotice(request, installedVersion, options = {}) {
|
|
148
|
+
if (!request || request.targetVersion !== installedVersion || !parseStableVersion(installedVersion)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const filePath = options.noticePath ?? changelogNoticePath(installedVersion);
|
|
152
|
+
const notice = {
|
|
153
|
+
schemaVersion: CHANGELOG_NOTICE_SCHEMA_VERSION,
|
|
154
|
+
packageName: "moondesk",
|
|
155
|
+
fromVersion: request.currentVersion,
|
|
156
|
+
toVersion: installedVersion,
|
|
157
|
+
releaseNotes: normalizePersistedReleaseNotes(request.releaseNotes),
|
|
158
|
+
releaseUrl: normalizeReleaseUrl(request.releaseUrl, installedVersion),
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
};
|
|
161
|
+
atomicWriteJson(filePath, notice, options);
|
|
162
|
+
return filePath;
|
|
163
|
+
}
|
|
164
|
+
|
|
104
165
|
function updateRequestDir() {
|
|
105
166
|
return path.join(currentUpdateDir(), "requests");
|
|
106
167
|
}
|
|
@@ -234,7 +295,7 @@ function atomicWriteJson(filePath, value, options = {}) {
|
|
|
234
295
|
}
|
|
235
296
|
}
|
|
236
297
|
|
|
237
|
-
async function fetchJsonLimited(fetchImpl, url, externalSignal) {
|
|
298
|
+
async function fetchJsonLimited(fetchImpl, url, externalSignal, maxBytes = MAX_UPDATE_METADATA_BYTES) {
|
|
238
299
|
const controller = new AbortController();
|
|
239
300
|
const abortFromParent = () => controller.abort();
|
|
240
301
|
if (externalSignal) {
|
|
@@ -259,7 +320,7 @@ async function fetchJsonLimited(fetchImpl, url, externalSignal) {
|
|
|
259
320
|
throw new Error(`${url} returned HTTP ${response.status}`);
|
|
260
321
|
}
|
|
261
322
|
const contentLength = Number(response.headers.get("content-length"));
|
|
262
|
-
if (Number.isFinite(contentLength) && contentLength >
|
|
323
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
263
324
|
controller.abort();
|
|
264
325
|
throw new Error(`MoonDesk update metadata is unexpectedly large (${contentLength} bytes)`);
|
|
265
326
|
}
|
|
@@ -272,7 +333,7 @@ async function fetchJsonLimited(fetchImpl, url, externalSignal) {
|
|
|
272
333
|
for await (const chunk of response.body) {
|
|
273
334
|
const buffer = Buffer.from(chunk);
|
|
274
335
|
totalBytes += buffer.length;
|
|
275
|
-
if (totalBytes >
|
|
336
|
+
if (totalBytes > maxBytes) {
|
|
276
337
|
controller.abort();
|
|
277
338
|
throw new Error("MoonDesk update metadata exceeded the download limit");
|
|
278
339
|
}
|
|
@@ -285,13 +346,138 @@ async function fetchJsonLimited(fetchImpl, url, externalSignal) {
|
|
|
285
346
|
}
|
|
286
347
|
}
|
|
287
348
|
|
|
288
|
-
|
|
349
|
+
function normalizeChangelogLine(line) {
|
|
350
|
+
if (typeof line !== "string") return null;
|
|
351
|
+
let value = line.trim();
|
|
352
|
+
if (!value || value.startsWith("#") || /^\*\*Full Changelog\*\*/i.test(value)) return null;
|
|
353
|
+
value = value.replace(/^[-*+]\s+/, "");
|
|
354
|
+
value = value.replace(GITHUB_PULL_ATTRIBUTION_RE, "");
|
|
355
|
+
value = value.replace(/^\[(.+?)\]\([^)]*\)$/, "$1");
|
|
356
|
+
value = value.replace(/`([^`]+)`/g, "$1");
|
|
357
|
+
value = value.replace(/^(?:feat|fix|chore|refactor|perf|docs|test|build|ci|style)(?:\([^)]*\))?!?:\s*/i, "");
|
|
358
|
+
value = value.trim();
|
|
359
|
+
if (!value || /^https?:\/\//i.test(value)) return null;
|
|
360
|
+
value = `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
361
|
+
if (value.length > MAX_CHANGELOG_ITEM_CHARS) {
|
|
362
|
+
value = `${value.slice(0, MAX_CHANGELOG_ITEM_CHARS - 3).trimEnd()}...`;
|
|
363
|
+
}
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function normalizeReleaseNotes(body) {
|
|
368
|
+
if (typeof body !== "string") return [];
|
|
369
|
+
const notes = [];
|
|
370
|
+
const seen = new Set();
|
|
371
|
+
for (const line of body.split(/\r?\n/)) {
|
|
372
|
+
const note = normalizeChangelogLine(line);
|
|
373
|
+
if (!note || seen.has(note)) continue;
|
|
374
|
+
seen.add(note);
|
|
375
|
+
notes.push(note);
|
|
376
|
+
if (notes.length >= MAX_CHANGELOG_ITEMS) break;
|
|
377
|
+
}
|
|
378
|
+
return notes;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function boundedChangelogItem(value) {
|
|
382
|
+
const trimmed = String(value ?? "").trim();
|
|
383
|
+
if (!trimmed) return null;
|
|
384
|
+
if (trimmed.length <= MAX_CHANGELOG_ITEM_CHARS) return trimmed;
|
|
385
|
+
return `${trimmed.slice(0, MAX_CHANGELOG_ITEM_CHARS - 3).trimEnd()}...`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function stableReleaseVersion(release) {
|
|
389
|
+
if (!release || release.draft === true || release.prerelease === true) return null;
|
|
390
|
+
if (typeof release.tag_name !== "string" || !release.tag_name.startsWith("v")) return null;
|
|
391
|
+
const version = release.tag_name.slice(1);
|
|
392
|
+
return parseStableVersion(version) ? version : null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function fetchReleaseChangelog(fromVersion, toVersion, options = {}) {
|
|
396
|
+
if (
|
|
397
|
+
!parseStableVersion(fromVersion) ||
|
|
398
|
+
!parseStableVersion(toVersion) ||
|
|
399
|
+
compareStableVersions(toVersion, fromVersion) <= 0
|
|
400
|
+
) {
|
|
401
|
+
return { releaseNotes: [], releaseUrl: null };
|
|
402
|
+
}
|
|
403
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
404
|
+
if (typeof fetchImpl !== "function") return { releaseNotes: [], releaseUrl: null };
|
|
405
|
+
|
|
406
|
+
const releasesUrl = options.releasesApiUrl ?? GITHUB_RELEASES_API_URL;
|
|
407
|
+
const releases = await fetchJsonLimited(
|
|
408
|
+
fetchImpl,
|
|
409
|
+
releasesUrl,
|
|
410
|
+
options.signal,
|
|
411
|
+
MAX_CHANGELOG_METADATA_BYTES,
|
|
412
|
+
);
|
|
413
|
+
if (!Array.isArray(releases)) {
|
|
414
|
+
throw new Error("GitHub returned invalid MoonDesk releases metadata");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const selected = [];
|
|
418
|
+
for (const release of releases) {
|
|
419
|
+
const version = stableReleaseVersion(release);
|
|
420
|
+
if (!version) continue;
|
|
421
|
+
if (
|
|
422
|
+
compareStableVersions(version, fromVersion) > 0 &&
|
|
423
|
+
compareStableVersions(version, toVersion) <= 0
|
|
424
|
+
) {
|
|
425
|
+
selected.push({ release, version });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!selected.some((item) => item.version === toVersion)) {
|
|
430
|
+
const tagApiBase = options.releaseTagApiBase ?? GITHUB_RELEASE_TAG_API_BASE;
|
|
431
|
+
try {
|
|
432
|
+
const release = await fetchJsonLimited(
|
|
433
|
+
fetchImpl,
|
|
434
|
+
`${tagApiBase}/v${toVersion}`,
|
|
435
|
+
options.signal,
|
|
436
|
+
MAX_CHANGELOG_METADATA_BYTES,
|
|
437
|
+
);
|
|
438
|
+
if (stableReleaseVersion(release) === toVersion) {
|
|
439
|
+
selected.push({ release, version: toVersion });
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
// The recent releases list may briefly lag the just-published npm tag.
|
|
443
|
+
// Missing notes must never block a valid npm update.
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
selected.sort((left, right) => compareStableVersions(right.version, left.version));
|
|
448
|
+
const uniqueReleases = [];
|
|
449
|
+
const seenVersions = new Set();
|
|
450
|
+
for (const item of selected) {
|
|
451
|
+
if (seenVersions.has(item.version)) continue;
|
|
452
|
+
seenVersions.add(item.version);
|
|
453
|
+
uniqueReleases.push(item);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const includeVersion = uniqueReleases.length > 1;
|
|
457
|
+
const releaseNotes = [];
|
|
458
|
+
const seenNotes = new Set();
|
|
459
|
+
for (const { release, version } of uniqueReleases) {
|
|
460
|
+
for (const note of normalizeReleaseNotes(release.body)) {
|
|
461
|
+
const rendered = boundedChangelogItem(includeVersion ? `v${version}: ${note}` : note);
|
|
462
|
+
if (!rendered || seenNotes.has(rendered)) continue;
|
|
463
|
+
seenNotes.add(rendered);
|
|
464
|
+
releaseNotes.push(rendered);
|
|
465
|
+
if (releaseNotes.length >= MAX_CHANGELOG_ITEMS) break;
|
|
466
|
+
}
|
|
467
|
+
if (releaseNotes.length >= MAX_CHANGELOG_ITEMS) break;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const expectedUrl = `${GITHUB_RELEASE_WEB_BASE}/v${toVersion}`;
|
|
471
|
+
const target = uniqueReleases.find((item) => item.version === toVersion)?.release;
|
|
472
|
+
const releaseUrl = target ? expectedUrl : null;
|
|
473
|
+
return { releaseNotes, releaseUrl };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function fetchLatestPackageMetadata(options = {}) {
|
|
289
477
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
290
|
-
const statePath = options.statePath ?? createUpdateStatePath();
|
|
291
478
|
const registryUrl = options.registryUrl ?? REGISTRY_LATEST_URL;
|
|
292
|
-
const managedInstall = options.managedInstall === true;
|
|
293
479
|
if (typeof fetchImpl !== "function") {
|
|
294
|
-
|
|
480
|
+
throw new Error("MoonDesk update checks require a fetch implementation");
|
|
295
481
|
}
|
|
296
482
|
|
|
297
483
|
const metadata = await fetchJsonLimited(fetchImpl, registryUrl, options.signal);
|
|
@@ -305,8 +491,69 @@ async function checkForUpdate(options = {}) {
|
|
|
305
491
|
if (typeof metadata?.dist?.integrity !== "string" || !metadata.dist.integrity.startsWith("sha512-")) {
|
|
306
492
|
throw new Error("npm returned MoonDesk metadata without a sha512 package integrity value");
|
|
307
493
|
}
|
|
494
|
+
return metadata;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function refreshUpdateRequestToLatest(request, options = {}) {
|
|
498
|
+
if (
|
|
499
|
+
!request ||
|
|
500
|
+
request.currentVersion !== currentVersion ||
|
|
501
|
+
!parseStableVersion(request.targetVersion) ||
|
|
502
|
+
compareStableVersions(request.targetVersion, currentVersion) <= 0
|
|
503
|
+
) {
|
|
504
|
+
throw new Error("Refusing to refresh an invalid MoonDesk update request");
|
|
505
|
+
}
|
|
308
506
|
|
|
507
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
508
|
+
const metadata = await fetchLatestPackageMetadata({ ...options, fetchImpl });
|
|
509
|
+
const latestVersion = metadata.version;
|
|
510
|
+
if (compareStableVersions(latestVersion, request.targetVersion) <= 0) {
|
|
511
|
+
return request;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let releaseNotes = [];
|
|
515
|
+
let releaseUrl = null;
|
|
516
|
+
try {
|
|
517
|
+
({ releaseNotes, releaseUrl } = await fetchReleaseChangelog(
|
|
518
|
+
request.currentVersion,
|
|
519
|
+
latestVersion,
|
|
520
|
+
{ ...options, fetchImpl },
|
|
521
|
+
));
|
|
522
|
+
} catch {
|
|
523
|
+
// The exact newest npm target is authoritative. GitHub notes remain optional.
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
...request,
|
|
528
|
+
targetVersion: latestVersion,
|
|
529
|
+
releaseNotes,
|
|
530
|
+
releaseUrl,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function checkForUpdate(options = {}) {
|
|
535
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
536
|
+
const statePath = options.statePath ?? createUpdateStatePath();
|
|
537
|
+
const managedInstall = options.managedInstall === true;
|
|
538
|
+
if (typeof fetchImpl !== "function") {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const metadata = await fetchLatestPackageMetadata({ ...options, fetchImpl });
|
|
543
|
+
const latestVersion = metadata.version;
|
|
309
544
|
const available = managedInstall && compareStableVersions(latestVersion, currentVersion) > 0;
|
|
545
|
+
let releaseNotes = [];
|
|
546
|
+
let releaseUrl = null;
|
|
547
|
+
if (available) {
|
|
548
|
+
try {
|
|
549
|
+
({ releaseNotes, releaseUrl } = await fetchReleaseChangelog(currentVersion, latestVersion, {
|
|
550
|
+
...options,
|
|
551
|
+
fetchImpl,
|
|
552
|
+
}));
|
|
553
|
+
} catch {
|
|
554
|
+
// Release notes are optional metadata. A GitHub outage must never hide a valid npm update.
|
|
555
|
+
}
|
|
556
|
+
}
|
|
310
557
|
const state = {
|
|
311
558
|
schemaVersion: UPDATE_STATE_SCHEMA_VERSION,
|
|
312
559
|
packageName: "moondesk",
|
|
@@ -314,6 +561,8 @@ async function checkForUpdate(options = {}) {
|
|
|
314
561
|
latestVersion,
|
|
315
562
|
managedInstall,
|
|
316
563
|
available,
|
|
564
|
+
releaseNotes,
|
|
565
|
+
releaseUrl,
|
|
317
566
|
checkedAt: new Date().toISOString(),
|
|
318
567
|
};
|
|
319
568
|
atomicWriteJson(statePath, state, options);
|
|
@@ -396,7 +645,11 @@ function readUpdateRequest(requestPath) {
|
|
|
396
645
|
) {
|
|
397
646
|
return null;
|
|
398
647
|
}
|
|
399
|
-
return
|
|
648
|
+
return {
|
|
649
|
+
...parsed,
|
|
650
|
+
releaseNotes: normalizePersistedReleaseNotes(parsed.releaseNotes),
|
|
651
|
+
releaseUrl: normalizeReleaseUrl(parsed.releaseUrl, parsed.targetVersion),
|
|
652
|
+
};
|
|
400
653
|
}
|
|
401
654
|
|
|
402
655
|
function updateLockPath() {
|
|
@@ -570,17 +823,22 @@ module.exports = {
|
|
|
570
823
|
atomicWriteJson,
|
|
571
824
|
checkForUpdate,
|
|
572
825
|
cleanupOldUpdateVersions,
|
|
826
|
+
changelogNoticePath,
|
|
573
827
|
compareStableVersions,
|
|
574
828
|
createUpdateRequestPath,
|
|
575
829
|
createUpdateStatePath,
|
|
576
830
|
currentVersion,
|
|
831
|
+
fetchReleaseChangelog,
|
|
577
832
|
installExactVersion,
|
|
578
833
|
installedWrapperVersion,
|
|
579
834
|
isGlobalPackageInstall,
|
|
835
|
+
normalizeReleaseNotes,
|
|
580
836
|
parseStableVersion,
|
|
581
837
|
readUpdateRequest,
|
|
838
|
+
refreshUpdateRequestToLatest,
|
|
582
839
|
resolveGlobalNpmRoot,
|
|
583
840
|
restartUpdatedWrapper,
|
|
584
841
|
startUpdateMonitor,
|
|
585
842
|
verifyInstalledWrapperVersion,
|
|
843
|
+
writePostUpdateNotice,
|
|
586
844
|
};
|