dsh-plugin-update 0.4.0 → 0.5.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/dist/changelog.js CHANGED
@@ -15,8 +15,8 @@ const CHANGELOG_ALL_CATEGORIES = [
15
15
  "Removed",
16
16
  "Security"
17
17
  ];
18
- const CHANGELOG_MUST_SHOW = ["Added", "Fixed", "Changed"];
19
- const CHANGELOG_FOLDED = ["Deprecated", "Removed", "Security"];
18
+ const CHANGELOG_MUST_SHOW = ["Added", "Fixed", "Changed", "Security"];
19
+ const CHANGELOG_FOLDED = ["Deprecated", "Removed"];
20
20
  const CHANGELOG_CATEGORY_ZH = {
21
21
  Added: "\u65B0\u589E",
22
22
  Fixed: "\u4FEE\u590D",
@@ -28,6 +28,9 @@ const CHANGELOG_CATEGORY_ZH = {
28
28
  function emptySections() {
29
29
  return { Added: [], Fixed: [], Changed: [], Deprecated: [], Removed: [], Security: [] };
30
30
  }
31
+ function emptyCounts() {
32
+ return { Added: 0, Fixed: 0, Changed: 0, Deprecated: 0, Removed: 0, Security: 0 };
33
+ }
31
34
  function isCategoryName(v) {
32
35
  return CHANGELOG_ALL_CATEGORIES.includes(v);
33
36
  }
@@ -60,6 +63,119 @@ function truncateBullet(text) {
60
63
  if (t.length <= CHANGELOG_MAX_BULLET_CHARS) return t;
61
64
  return `${t.slice(0, CHANGELOG_MAX_BULLET_CHARS - 1)}\u2026`;
62
65
  }
66
+ const RE_VERSION_HEADING = /^##(?!#)\s*(.+?)\s*$/;
67
+ const RE_CATEGORY_HEADING = /^###\s*(.+?)\s*$/;
68
+ const RE_BULLET = /^\s*[-*+]\s+(.+?)\s*$/;
69
+ const RE_FENCE_TOGGLE = new RegExp("^\\s*(" + String.fromCharCode(96).repeat(3) + "|~~~)");
70
+ const RE_VERSION_NUM = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/;
71
+ const RE_VERSION_DATE = /(\d{4}-\d{2}-\d{2})/;
72
+ const RE_YANKED = /\[YANKED\]/i;
73
+ const RE_UNRELEASED = /unreleased/i;
74
+ function matchVersionHeading(line) {
75
+ try {
76
+ const s = String(line !== null && line !== void 0 ? line : "");
77
+ const m = s.match(RE_VERSION_HEADING);
78
+ if (!m) return null;
79
+ return String(m[1] !== void 0 && m[1] !== null ? m[1] : "").trim();
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ function matchCategoryHeading(line) {
85
+ try {
86
+ const s = String(line !== null && line !== void 0 ? line : "");
87
+ const m = s.match(RE_CATEGORY_HEADING);
88
+ if (!m) return null;
89
+ return String(m[1] !== void 0 && m[1] !== null ? m[1] : "").trim();
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ function matchBulletBody(line) {
95
+ try {
96
+ const s = String(line !== null && line !== void 0 ? line : "");
97
+ const m = s.match(RE_BULLET);
98
+ if (!m) return null;
99
+ return String(m[1] !== void 0 && m[1] !== null ? m[1] : "");
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ function isFenceToggle(line) {
105
+ try {
106
+ return RE_FENCE_TOGGLE.test(String(line !== null && line !== void 0 ? line : ""));
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ const BREAKING_BADGE_TEXT = "\u4E0D\u517C\u5BB9";
112
+ function stripBreakingDecorations(s) {
113
+ try {
114
+ let t = String(s !== null && s !== void 0 ? s : "").trim();
115
+ let changed = true;
116
+ while (changed) {
117
+ changed = false;
118
+ if (t.charAt(0) === ">") {
119
+ t = t.slice(1).trim();
120
+ changed = true;
121
+ continue;
122
+ }
123
+ if (t.slice(0, 2) === "**" || t.slice(0, 2) === "__") {
124
+ t = t.slice(2).trim();
125
+ changed = true;
126
+ continue;
127
+ }
128
+ if (t.charAt(0) === "*" || t.charAt(0) === "_") {
129
+ t = t.slice(1).trim();
130
+ changed = true;
131
+ continue;
132
+ }
133
+ }
134
+ return t;
135
+ } catch {
136
+ return "";
137
+ }
138
+ }
139
+ function isBreakingChangelogItem(item) {
140
+ try {
141
+ if (typeof item !== "string") return false;
142
+ const t = stripBreakingDecorations(item);
143
+ if (!t) return false;
144
+ return /^(breaking|不兼容)\s*[::]/i.test(t);
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ function splitBreakingPrefix(item) {
150
+ try {
151
+ const s = String(item !== null && item !== void 0 ? item : "");
152
+ const m = s.match(/^(\s*(?:>\s*|\*\*\s*|__\s*|\*\s*|_\s*)*)(breaking|不兼容)(\s*[::])/i);
153
+ if (!m) return null;
154
+ const head = String(m[1] !== void 0 && m[1] !== null ? m[1] : "");
155
+ const core = String(m[2] !== void 0 && m[2] !== null ? m[2] : "");
156
+ const colon = String(m[3] !== void 0 && m[3] !== null ? m[3] : "");
157
+ const prefix = core + colon;
158
+ const rest = s.slice(m[0].length);
159
+ return { head, prefix, rest };
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ const CHANGELOG_POLL_BACKOFF_MS = 30 * 1e3;
165
+ function shouldFetchChangelog(opts) {
166
+ try {
167
+ const o = opts;
168
+ if (o.hasCache === true) return false;
169
+ if (o.isManual === true) return true;
170
+ const now = typeof o.now === "number" && Number.isFinite(o.now) ? o.now : Date.now();
171
+ const failedAt = typeof o.failedAt === "number" && Number.isFinite(o.failedAt) ? o.failedAt : null;
172
+ if (failedAt === null) return true;
173
+ const backoff = typeof o.backoffMs === "number" && Number.isFinite(o.backoffMs) && o.backoffMs > 0 ? Math.floor(o.backoffMs) : CHANGELOG_POLL_BACKOFF_MS;
174
+ return now - failedAt >= backoff;
175
+ } catch {
176
+ return true;
177
+ }
178
+ }
63
179
  function parseChangelog(markdown) {
64
180
  try {
65
181
  if (typeof markdown !== "string" || !markdown.trim()) return [];
@@ -86,32 +202,38 @@ function parseChangelog(markdown) {
86
202
  };
87
203
  for (const rawLine of lines) {
88
204
  const line = String(rawLine ?? "");
89
- if (/^\s*(```|~~~)/.test(line)) {
205
+ if (isFenceToggle(line)) {
90
206
  inFence = !inFence;
91
207
  continue;
92
208
  }
93
209
  if (inFence) continue;
94
- const versionHeading = line.match(/^##(?!#)\s*(.+?)\s*$/);
95
- if (versionHeading) {
210
+ const versionTitle = matchVersionHeading(line);
211
+ if (versionTitle !== null) {
96
212
  pushCurrent();
97
213
  cur = null;
98
214
  curCat = null;
99
- const title = (versionHeading[1] ?? "").trim();
215
+ const title = versionTitle;
100
216
  const { version, date, yanked } = extractVersionDateAndYanked(title);
101
217
  if (!version) continue;
102
- cur = { version, date, yanked, sections: emptySections() };
218
+ cur = { version, date, yanked, sections: emptySections(), counts: emptyCounts() };
103
219
  continue;
104
220
  }
105
- const catMatch = line.match(/^###\s*(.+?)\s*$/);
106
- if (catMatch) {
107
- const cat = normalizeCategory(catMatch[1] ?? "");
221
+ const catTitle = matchCategoryHeading(line);
222
+ if (catTitle !== null) {
223
+ const cat = normalizeCategory(catTitle);
108
224
  curCat = cur && cat && isCategoryName(cat) ? cat : null;
109
225
  continue;
110
226
  }
111
- const bullet = line.match(/^\s*[-*+]\s+(.+?)\s*$/);
112
- if (bullet && cur && curCat) {
113
- const item = truncateBullet(bullet[1] ?? "");
114
- if (item) cur.sections[curCat].push(item);
227
+ const bulletBody = matchBulletBody(line);
228
+ if (bulletBody !== null && cur && curCat) {
229
+ const item = truncateBullet(bulletBody);
230
+ if (item) {
231
+ cur.sections[curCat].push(item);
232
+ try {
233
+ if (cur.counts) cur.counts[curCat] += 1;
234
+ } catch {
235
+ }
236
+ }
115
237
  continue;
116
238
  }
117
239
  const trimmed = line.trim();
@@ -189,46 +311,119 @@ function changelogForUpdate(entries, runningVersion, latestVersion, installedVer
189
311
  function escapeChangelogHtml(text) {
190
312
  return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
191
313
  }
314
+ function countsOf(entry) {
315
+ try {
316
+ const out = { Added: 0, Fixed: 0, Changed: 0, Deprecated: 0, Removed: 0, Security: 0 };
317
+ if (!entry || typeof entry !== "object") return out;
318
+ for (const c of CHANGELOG_ALL_CATEGORIES) {
319
+ const n = entry.counts;
320
+ const v = n && typeof n[c] === "number" && Number.isFinite(n[c]) ? Math.floor(n[c]) : null;
321
+ if (v !== null && v >= 0) {
322
+ out[c] = v;
323
+ continue;
324
+ }
325
+ const list = entry.sections;
326
+ const arr = list ? list[c] : null;
327
+ out[c] = Array.isArray(arr) ? arr.length : 0;
328
+ }
329
+ return out;
330
+ } catch {
331
+ return { Added: 0, Fixed: 0, Changed: 0, Deprecated: 0, Removed: 0, Security: 0 };
332
+ }
333
+ }
334
+ function truncatedNoteHTML(total, shown) {
335
+ try {
336
+ const m = Math.floor(total);
337
+ const n = Math.floor(shown);
338
+ return '<div class="dsh-upd-changelog-count">\u5171 ' + String(m) + " \u6761\uFF0C\u4EC5\u663E\u793A\u524D " + String(n) + " \u6761</div>";
339
+ } catch {
340
+ return "";
341
+ }
342
+ }
343
+ function yankedBannerHTML(version) {
344
+ try {
345
+ const v = typeof version === "string" ? version.trim() : "";
346
+ if (!v) return "";
347
+ return '<div class="dsh-upd-changelog-yanked" role="alert">\u76EE\u6807\u7248\u672C ' + escapeChangelogHtml(v) + " \u5DF2\u88AB\u4F5C\u8005\u64A4\u56DE\uFF08yanked\uFF09\uFF0C\u5B89\u88C5\u4E0D\u53D7\u5F71\u54CD\uFF0C\u7EE7\u7EED\u524D\u8BF7\u786E\u8BA4\u3002</div>";
348
+ } catch {
349
+ return "";
350
+ }
351
+ }
352
+ function renderChangelogItem(item) {
353
+ try {
354
+ const s = String(item !== null && item !== void 0 ? item : "");
355
+ if (!s) return "";
356
+ const split = splitBreakingPrefix(s);
357
+ if (!split) return "<li>" + escapeChangelogHtml(s) + "</li>";
358
+ const head = escapeChangelogHtml(split.head);
359
+ const prefix = escapeChangelogHtml(split.prefix);
360
+ const rest = escapeChangelogHtml(split.rest);
361
+ const badge = '<span class="dsh-upd-breaking-badge" role="img" aria-label="\u7834\u574F\u6027\u53D8\u66F4">' + BREAKING_BADGE_TEXT + "</span> ";
362
+ return "<li>" + head + badge + "<strong>" + prefix + "</strong>" + rest + "</li>";
363
+ } catch {
364
+ return "";
365
+ }
366
+ }
192
367
  function renderChangelogSection(entry) {
193
368
  try {
194
369
  if (!entry || typeof entry !== "object") return "";
195
- const version = String(entry.version ?? "").trim();
370
+ const version = String(entry.version !== void 0 && entry.version !== null ? String(entry.version) : "").trim();
196
371
  if (!version || isUnreleasedVersion(version)) return "";
197
372
  if (!hasVisibleSections(entry)) return "";
198
- const date = typeof entry.date === "string" ? String(entry.date) : "";
199
- const yankedSuffix = entry.yanked === true ? " \xB7 \u5DF2\u64A4\u56DE" : "";
200
- const title = (date ? `${version} \xB7 ${date}` : version) + yankedSuffix;
373
+ const dateRaw = entry.date;
374
+ const date = typeof dateRaw === "string" ? dateRaw : "";
375
+ const yankedFlag = entry.yanked === true;
376
+ const yankedSuffix = yankedFlag ? " \xB7 \u5DF2\u64A4\u56DE" : "";
377
+ const title = (date ? version + " \xB7 " + date : version) + yankedSuffix;
378
+ const counts = countsOf(entry);
201
379
  const parts = [];
202
- parts.push(`<div class="dsh-upd-changelog-version" data-version="${escapeChangelogHtml(version)}">`);
203
- parts.push(`<div class="dsh-upd-changelog-title">${escapeChangelogHtml(title)}</div>`);
380
+ parts.push('<div class="dsh-upd-changelog-version" data-version="' + escapeChangelogHtml(version) + '">');
381
+ parts.push('<div class="dsh-upd-changelog-title">' + escapeChangelogHtml(title) + "</div>");
204
382
  for (const cat of CHANGELOG_MUST_SHOW) {
205
- const items = Array.isArray(entry.sections?.[cat]) ? entry.sections[cat] : [];
206
- if (items.length === 0) continue;
207
- const label = `${cat} \xB7 ${CHANGELOG_CATEGORY_ZH[cat]}`;
208
- parts.push(`<div class="dsh-upd-changelog-cat" data-cat="${cat}">`);
209
- parts.push(`<div class="dsh-upd-changelog-catname">${escapeChangelogHtml(label)}</div>`);
383
+ const items = Array.isArray(entry.sections ? entry.sections[cat] : null) ? entry.sections[cat] : [];
384
+ if (!items || items.length === 0) continue;
385
+ const label = cat + " \xB7 " + CHANGELOG_CATEGORY_ZH[cat];
386
+ parts.push('<div class="dsh-upd-changelog-cat" data-cat="' + cat + '">');
387
+ parts.push('<div class="dsh-upd-changelog-catname">' + escapeChangelogHtml(label) + "</div>");
388
+ const total = counts[cat];
389
+ const shown = items.length;
390
+ if (total > shown) {
391
+ parts.push(truncatedNoteHTML(total, shown));
392
+ }
210
393
  parts.push("<ul>");
211
394
  for (const item of items) {
212
395
  if (!item) continue;
213
- parts.push(`<li>${escapeChangelogHtml(item)}</li>`);
396
+ const li = renderChangelogItem(item);
397
+ if (li) parts.push(li);
398
+ }
399
+ parts.push("</ul>");
400
+ if (cat === "Security" && total > shown) {
401
+ const restCount = total - shown;
402
+ parts.push('<details class="dsh-upd-changelog-security-more"><summary>\u5176\u4F59 ' + String(restCount) + ' \u6761</summary><div class="dsh-upd-changelog-more-note">\u4E3A\u4FDD\u6301\u9762\u677F\u6027\u80FD\uFF0C\u5176\u4F59\u6761\u76EE\u5DF2\u6298\u53E0\uFF0C\u53EF\u67E5\u770B\u539F\u6587\u3002</div></details>');
214
403
  }
215
- parts.push("</ul></div>");
404
+ parts.push("</div>");
216
405
  }
217
406
  for (const cat of CHANGELOG_FOLDED) {
218
- const items = Array.isArray(entry.sections?.[cat]) ? entry.sections[cat] : [];
219
- if (items.length === 0) continue;
220
- const label = `${cat} \xB7 ${CHANGELOG_CATEGORY_ZH[cat]}\uFF08${items.length}\uFF09`;
221
- parts.push(`<details class="dsh-upd-changelog-fold" data-cat="${cat}">`);
222
- parts.push(`<summary>${escapeChangelogHtml(label)}</summary>`);
407
+ const items = Array.isArray(entry.sections ? entry.sections[cat] : null) ? entry.sections[cat] : [];
408
+ if (!items || items.length === 0) continue;
409
+ const label = cat + " \xB7 " + CHANGELOG_CATEGORY_ZH[cat] + "\uFF08" + String(items.length) + "\uFF09";
410
+ parts.push('<details class="dsh-upd-changelog-fold" data-cat="' + cat + '">');
411
+ parts.push("<summary>" + escapeChangelogHtml(label) + "</summary>");
412
+ const total = counts[cat];
413
+ const shown = items.length;
414
+ if (total > shown) {
415
+ parts.push(truncatedNoteHTML(total, shown));
416
+ }
223
417
  parts.push("<ul>");
224
418
  for (const item of items) {
225
419
  if (!item) continue;
226
- parts.push(`<li>${escapeChangelogHtml(item)}</li>`);
420
+ const li = renderChangelogItem(item);
421
+ if (li) parts.push(li);
227
422
  }
228
423
  parts.push("</ul></details>");
229
424
  }
230
425
  parts.push("</div>");
231
- return parts.join("\n");
426
+ return parts.join(String.fromCharCode(10));
232
427
  } catch {
233
428
  return "";
234
429
  }
@@ -264,7 +459,165 @@ ${blocks.join("\n")}
264
459
  return renderChangelogNeutral();
265
460
  }
266
461
  }
462
+ const CHANGELOG_VALIDATE_HINTS = {
463
+ E_VERSION_TITLE: "\u8BE5\u6807\u9898\u4E0D\u662F\u5408\u6CD5\u7248\u672C\u53F7\uFF0C\u5176\u4E0B\u6761\u76EE\u5DF2\u88AB\u5FFD\u7565\uFF0C\u8BF7\u6539\u6210 ## [x.y.z] \u5F62\u6001",
464
+ E_CATEGORY: "\u672A\u77E5\u5206\u7C7B\uFF0C\u5176\u4E0B\u6761\u76EE\u5DF2\u88AB\u5FFD\u7565\uFF0C\u4EC5\u6536 Added/Fixed/Changed/Deprecated/Removed/Security",
465
+ E_BULLET_ORPHAN: "\u8BE5\u6761\u76EE\u4E0D\u5728\u7248\u672C\u8282\u4E0E\u5206\u7C7B\u4E0B\uFF0C\u5DF2\u88AB\u5FFD\u7565",
466
+ W_BREAKING_MAYBE: "\u7591\u4F3C\u7834\u574F\u6807\u8BB0\u8BEF\u5199\uFF08\u7F3A\u5192\u53F7/\u62FC\u5199\u63A5\u8FD1/\u4F4D\u7F6E\u7591\u4F3C\u653E\u9519\uFF09\uFF0C\u8BF7\u68C0\u67E5\u662F\u5426\u60F3\u5199 BREAKING:/\u4E0D\u517C\u5BB9:",
467
+ W_YANKED: "\u8BE5\u7248\u672C\u5DF2\u88AB\u6807\u8BB0\u64A4\u56DE\uFF08yanked\uFF09\uFF0C\u5B89\u88C5\u4E0D\u53D7\u5F71\u54CD\uFF0C\u7EE7\u7EED\u524D\u8BF7\u786E\u8BA4",
468
+ W_TRUNCATED: "\u8BE5\u7C7B\u5171 M \u6761\uFF0C\u4EC5\u663E\u793A\u524D N \u6761",
469
+ W_TRUNCATED_FILE: "\u6587\u4EF6\u8D85 64K\uFF0C\u4EC5\u6821\u9A8C\u524D 64K",
470
+ W_EMPTY: "\u672A\u89E3\u6790\u5230\u4EFB\u4F55\u7248\u672C\u8282\uFF0C\u9762\u677F\u5C06\u663E\u793A\u4E2D\u6027\u63D0\u793A"
471
+ };
472
+ function isMaybeBreakingMisuse(bulletBody) {
473
+ try {
474
+ if (typeof bulletBody !== "string") return false;
475
+ const t = stripBreakingDecorations(bulletBody);
476
+ if (!t) return false;
477
+ if (isBreakingChangelogItem(bulletBody)) return false;
478
+ if (/^breaking\s*$/i.test(t)) return true;
479
+ if (/^breaking\s+[^::]/i.test(t)) return true;
480
+ if (/^breaking[^::\s\w]/i.test(t)) return true;
481
+ if (/^不兼容\s*$/i.test(t)) return true;
482
+ if (/^不兼容\s*[^::]/i.test(t)) return true;
483
+ if (/^(braking|breking|breakign|brekaing|braeking|breakingg)\b/i.test(t)) return true;
484
+ if (/^break[^i::\s]/i.test(t)) return true;
485
+ return false;
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+ function validateChangelog(markdown) {
491
+ try {
492
+ if (typeof markdown !== "string" || !markdown.trim()) {
493
+ return { ok: true, diagnostics: [{ line: 1, code: "W_EMPTY", hint: CHANGELOG_VALIDATE_HINTS.W_EMPTY }] };
494
+ }
495
+ let raw = String(markdown).replace(/\r\n/g, String.fromCharCode(10));
496
+ let fileTruncated = false;
497
+ if (raw.length > CHANGELOG_MAX_CHARS) {
498
+ fileTruncated = true;
499
+ raw = raw.slice(0, CHANGELOG_MAX_CHARS);
500
+ }
501
+ const lines = raw.split(String.fromCharCode(10));
502
+ const diags = [];
503
+ let inFence = false;
504
+ let curValid = false;
505
+ let curCat = null;
506
+ let catLineByCat = {};
507
+ let countByCat = {};
508
+ let hasVisibleVersion = false;
509
+ const flushCounts = function() {
510
+ try {
511
+ for (const k of Object.keys(countByCat)) {
512
+ const n = countByCat[k];
513
+ if (n > CHANGELOG_MAX_BULLETS_PER_SECTION) {
514
+ const lineNo = catLineByCat[k] ? catLineByCat[k] : lines.length;
515
+ const hint = "\u5206\u7C7B " + k + " \u5171 " + String(n) + " \u6761\uFF0C\u4EC5\u663E\u793A\u524D " + String(CHANGELOG_MAX_BULLETS_PER_SECTION) + " \u6761";
516
+ diags.push({ line: lineNo, code: "W_TRUNCATED", hint });
517
+ }
518
+ }
519
+ } catch {
520
+ }
521
+ };
522
+ for (let i = 0; i < lines.length; i++) {
523
+ const lineNo = i + 1;
524
+ const line = String(lines[i] !== void 0 && lines[i] !== null ? lines[i] : "");
525
+ if (isFenceToggle(line)) {
526
+ inFence = !inFence;
527
+ continue;
528
+ }
529
+ if (inFence) continue;
530
+ const vTitle = matchVersionHeading(line);
531
+ if (vTitle !== null) {
532
+ flushCounts();
533
+ catLineByCat = {};
534
+ countByCat = {};
535
+ curValid = false;
536
+ curCat = null;
537
+ const info = extractVersionDateAndYanked(vTitle);
538
+ if (!info.version) {
539
+ diags.push({ line: lineNo, code: "E_VERSION_TITLE", hint: CHANGELOG_VALIDATE_HINTS.E_VERSION_TITLE });
540
+ continue;
541
+ }
542
+ if (isUnreleasedVersion(info.version)) {
543
+ curValid = false;
544
+ continue;
545
+ }
546
+ curValid = true;
547
+ hasVisibleVersion = true;
548
+ if (info.yanked) {
549
+ diags.push({ line: lineNo, code: "W_YANKED", hint: CHANGELOG_VALIDATE_HINTS.W_YANKED });
550
+ }
551
+ continue;
552
+ }
553
+ const cTitle = matchCategoryHeading(line);
554
+ if (cTitle !== null) {
555
+ const cat = normalizeCategory(cTitle);
556
+ if (!cat) {
557
+ curCat = null;
558
+ diags.push({ line: lineNo, code: "E_CATEGORY", hint: CHANGELOG_VALIDATE_HINTS.E_CATEGORY });
559
+ continue;
560
+ }
561
+ if (!curValid) {
562
+ curCat = null;
563
+ continue;
564
+ }
565
+ curCat = cat;
566
+ catLineByCat[cat] = lineNo;
567
+ if (countByCat[cat] === void 0) countByCat[cat] = 0;
568
+ continue;
569
+ }
570
+ const body = matchBulletBody(line);
571
+ if (body !== null) {
572
+ if (!curValid || !curCat) {
573
+ diags.push({ line: lineNo, code: "E_BULLET_ORPHAN", hint: CHANGELOG_VALIDATE_HINTS.E_BULLET_ORPHAN });
574
+ continue;
575
+ }
576
+ countByCat[curCat] = (countByCat[curCat] ? countByCat[curCat] : 0) + 1;
577
+ if (isMaybeBreakingMisuse(body)) {
578
+ diags.push({ line: lineNo, code: "W_BREAKING_MAYBE", hint: CHANGELOG_VALIDATE_HINTS.W_BREAKING_MAYBE });
579
+ }
580
+ continue;
581
+ }
582
+ }
583
+ flushCounts();
584
+ if (fileTruncated) {
585
+ diags.push({ line: lines.length, code: "W_TRUNCATED_FILE", hint: CHANGELOG_VALIDATE_HINTS.W_TRUNCATED_FILE });
586
+ }
587
+ if (!hasVisibleVersion) {
588
+ let anyVisible = false;
589
+ try {
590
+ const parsed = parseChangelog(markdown);
591
+ for (const e of parsed) {
592
+ if (e && !isUnreleasedVersion(e.version) && hasVisibleSections(e)) {
593
+ anyVisible = true;
594
+ break;
595
+ }
596
+ }
597
+ } catch {
598
+ anyVisible = false;
599
+ }
600
+ if (!anyVisible) {
601
+ diags.push({ line: lines.length, code: "W_EMPTY", hint: CHANGELOG_VALIDATE_HINTS.W_EMPTY });
602
+ }
603
+ }
604
+ diags.sort(function(a, b) {
605
+ return a.line - b.line;
606
+ });
607
+ let ok = true;
608
+ for (const d of diags) {
609
+ if (d.code.charAt(0) === "E") {
610
+ ok = false;
611
+ break;
612
+ }
613
+ }
614
+ return { ok, diagnostics: diags };
615
+ } catch {
616
+ return { ok: false, diagnostics: [] };
617
+ }
618
+ }
267
619
  export {
620
+ BREAKING_BADGE_TEXT,
268
621
  CHANGELOG_ALL_CATEGORIES,
269
622
  CHANGELOG_CATEGORY_ZH,
270
623
  CHANGELOG_FILENAME,
@@ -276,12 +629,36 @@ export {
276
629
  CHANGELOG_MUST_SHOW,
277
630
  CHANGELOG_NEUTRAL_HINT,
278
631
  CHANGELOG_NEUTRAL_LINE,
632
+ CHANGELOG_POLL_BACKOFF_MS,
633
+ CHANGELOG_VALIDATE_HINTS,
634
+ RE_BULLET,
635
+ RE_CATEGORY_HEADING,
636
+ RE_FENCE_TOGGLE,
637
+ RE_UNRELEASED,
638
+ RE_VERSION_DATE,
639
+ RE_VERSION_HEADING,
640
+ RE_VERSION_NUM,
641
+ RE_YANKED,
279
642
  changelogForUpdate,
643
+ countsOf,
280
644
  hasVisibleSections,
645
+ isBreakingChangelogItem,
646
+ isFenceToggle,
647
+ isMaybeBreakingMisuse,
281
648
  isUnreleasedVersion,
649
+ matchBulletBody,
650
+ matchCategoryHeading,
651
+ matchVersionHeading,
282
652
  parseChangelog,
283
653
  renderChangelogHTML,
654
+ renderChangelogItem,
284
655
  renderChangelogNeutral,
285
656
  renderChangelogSection,
286
- selectChangelogEntries
657
+ selectChangelogEntries,
658
+ shouldFetchChangelog,
659
+ splitBreakingPrefix,
660
+ stripBreakingDecorations,
661
+ truncatedNoteHTML,
662
+ validateChangelog,
663
+ yankedBannerHTML
287
664
  };
package/dist/entry.d.ts CHANGED
@@ -5,7 +5,11 @@ export type EntryVariant = 'button' | 'badge' | 'inline';
5
5
  /** 什么时候主动查一次(只读,联网一次)。 */
6
6
  export type EntryAutoCheck = 'mount' | 'never';
7
7
  /** 用户点下去之后做什么。 */
8
- export type EntryOpenOn = 'has-update' | 'always' | 'manual';
8
+ /**
9
+ * 点下去之后做什么:`has-update` 有新版才开面板(无事只给一句小字);`always` 检查完总是开;
10
+ * `manual` 交给 onActivate;`direct` 点开即弹窗、不预查(面板挂载即自查;badge 仍走回调口径)。
11
+ */
12
+ export type EntryOpenOn = 'has-update' | 'always' | 'manual' | 'direct';
9
13
  /** 入口件主题(与面板同一套取值,切换即换肤)。 */
10
14
  export type EntryTheme = UpdatePanelTheme;
11
15
  /** 入口件的状态档:只用来算文案与 data-state,不另立状态机。 */
@@ -22,7 +26,8 @@ export interface UpdateEntryOptions {
22
26
  /** 调宿主电话:(phoneName, args) => Promise<reply>。 */
23
27
  call: (name: string, args: Record<string, unknown>) => Promise<unknown>;
24
28
  variant?: EntryVariant;
25
- theme?: 'default' | 'd5-paper';
29
+ /** 主题:`default` 最小可用深色,`archive` 档案卷纸面浅色(`d5-paper` 为旧别名仍可用)。 */
30
+ theme?: UpdatePanelTheme;
26
31
  /** 缺省 'mount':进页面静默查一次(只读)。'never' 则只在用户点击时查。 */
27
32
  autoCheck?: EntryAutoCheck;
28
33
  /** 缺省 'has-update':有新版才开面板,没有就只在原地给一句提示。 */
@@ -52,7 +57,7 @@ export interface UpdateEntryController {
52
57
  close(): void;
53
58
  /** 当前按钮上的状态文案(接入方做自定义排版时读它)。 */
54
59
  label(): string;
55
- setTheme(theme: 'default' | 'd5-paper'): void;
60
+ setTheme(theme: UpdatePanelTheme): void;
56
61
  unmount(): void;
57
62
  }
58
63
  /**
package/dist/entry.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { DEFAULT_PREFIX, MIN_PANEL_POLL_MS, buildPhoneNames } from "./config.js";
3
3
  import {
4
4
  failureCodeOf,
5
- mountUpdatePanel
5
+ mountUpdatePanel,
6
+ normalizePanelTheme
6
7
  } from "./panel.js";
7
8
  const LABEL_IDLE = "\u68C0\u67E5\u66F4\u65B0";
8
9
  const LABEL_FAILED = "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B";
@@ -63,10 +64,15 @@ const UPDATE_ENTRY_CSS = [
63
64
  '.dsh-upd-entry[data-state="update"] .dsh-upd-entry-dot{background:var(--dsh-upd-ok-line,#059669)}',
64
65
  '.dsh-upd-entry[data-state="busy"] .dsh-upd-entry-dot,.dsh-upd-entry[data-state="restart"] .dsh-upd-entry-dot{background:var(--dsh-upd-warn-line,#d97706)}',
65
66
  '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-dot{background:var(--dsh-upd-bad-line,#dc2626)}',
66
- ".dsh-upd-entry-note{font-size:12.5px;opacity:.75}",
67
- '.dsh-upd-entry[data-theme="d5-paper"]{font-family:Georgia,"Songti SC","STSong","SimSun",serif;color:#1a1a1a}',
68
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn{border-color:#c4b896;background:transparent;border-radius:3px}',
69
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn:hover{border-color:#c8402a;color:#c8402a}',
67
+ // 小字自带底(深色宿主 + 浅色变量时也读得出;浅底宿主上只是多一圈细线,不抢戏)。
68
+ ".dsh-upd-entry-note{font-size:12.5px;opacity:.9;background:var(--dsh-upd-bg,#ffffff);border:1px solid var(--dsh-upd-line,#e5e7eb);border-radius:4px;padding:1px 8px}",
69
+ '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-note{background:var(--d5-card);border-color:var(--d5-line-strong);color:var(--d5-ink)}',
70
+ '.dsh-upd-entry[data-theme="d5-paper"]{--d5-ink:#1a1a1a;--d5-muted:#6f675a;--d5-line-strong:#c4b896;--d5-accent:#c8402a;--d5-card:#fffdf6;',
71
+ 'font-family:Georgia,"Songti SC","STSong","SimSun",serif;color:var(--d5-ink)}',
72
+ // 按钮脸自己不透明(深色宿主 + 浅色系统变量时也读得出;hover 红在深浅底上都可见)。
73
+ '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px}',
74
+ '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn:hover{border-color:var(--d5-accent);color:var(--d5-accent)}',
75
+ '@media (prefers-color-scheme: dark){.dsh-upd-entry[data-theme="d5-paper"]{--d5-ink:#ece5d3;--d5-muted:#a89c83;--d5-line-strong:#5c4e3b;--d5-accent:#e0684e;--d5-card:#1e1a15}}',
70
76
  "@media (prefers-color-scheme: dark){.dsh-upd-entry{color:#e5e7eb}",
71
77
  ".dsh-upd-entry-btn{--dsh-upd-btn:#1f2937;--dsh-upd-line:#374151}}"
72
78
  ].join("\n");
@@ -110,13 +116,13 @@ function mountUpdateEntry(container, options) {
110
116
  throw new Error(`[dsh-plugin-update] \u81EA\u52A8\u68C0\u67E5\u65F6\u673A\u975E\u6CD5\uFF1A\u53EA\u6536 mount \u6216 never\uFF08\u6536\u5230 ${JSON.stringify(options.autoCheck)}\uFF09`);
111
117
  }
112
118
  const openOn = options.openOn ?? "has-update";
113
- if (openOn !== "has-update" && openOn !== "always" && openOn !== "manual") {
114
- throw new Error(`[dsh-plugin-update] \u70B9\u51FB\u53BB\u5411\u975E\u6CD5\uFF1A\u53EA\u6536 has-update / always / manual\uFF08\u6536\u5230 ${JSON.stringify(options.openOn)}\uFF09`);
119
+ if (openOn !== "has-update" && openOn !== "always" && openOn !== "manual" && openOn !== "direct") {
120
+ throw new Error(`[dsh-plugin-update] \u70B9\u51FB\u53BB\u5411\u975E\u6CD5\uFF1A\u53EA\u6536 has-update / always / manual / direct\uFF08\u6536\u5230 ${JSON.stringify(options.openOn)}\uFF09`);
115
121
  }
116
- let theme = options.theme ?? "default";
117
- if (theme !== "default" && theme !== "d5-paper") {
118
- throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 d5-paper\uFF08\u6536\u5230 ${JSON.stringify(options.theme)}\uFF09`);
122
+ if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive" && options.theme !== "d5-paper") {
123
+ throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08d5-paper \u4E3A\u65E7\u522B\u540D\u4ECD\u53EF\u7528\uFF09\uFF08\u6536\u5230 ${JSON.stringify(options.theme)}\uFF09`);
119
124
  }
125
+ let theme = normalizePanelTheme(options.theme ?? "default");
120
126
  const pollMs = options.pollMs;
121
127
  if (pollMs !== void 0 && (typeof pollMs !== "number" || !Number.isFinite(pollMs) || pollMs < MIN_PANEL_POLL_MS)) {
122
128
  throw new Error(`[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2\u95F4\u9694\u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 ${JSON.stringify(options.pollMs)}\uFF09`);
@@ -219,6 +225,8 @@ function mountUpdateEntry(container, options) {
219
225
  profileName,
220
226
  autoChangelog: options.autoChangelog,
221
227
  changelogMarkdown: options.changelogMarkdown ?? null,
228
+ // 面板点「关闭」即走入口件的完整关闭(收 dialog + 还原按钮 + 重查一次),不再是面板自己停轮询。
229
+ onCloseRequested: () => close(),
222
230
  call: panelCall
223
231
  });
224
232
  panelMode = mode;
@@ -245,6 +253,10 @@ function mountUpdateEntry(container, options) {
245
253
  }
246
254
  async function activate() {
247
255
  if (!mounted || panelMode !== null || activating) return;
256
+ if (openOn === "direct" && variant === "button") {
257
+ openDialog();
258
+ return;
259
+ }
248
260
  activating = true;
249
261
  note = null;
250
262
  render();
@@ -268,11 +280,11 @@ function mountUpdateEntry(container, options) {
268
280
  render();
269
281
  }
270
282
  function setTheme(next) {
271
- if (next !== "default" && next !== "d5-paper") {
272
- throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 d5-paper\uFF08\u6536\u5230 ${JSON.stringify(next)}\uFF09`);
283
+ if (next !== "default" && next !== "archive" && next !== "d5-paper") {
284
+ throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08d5-paper \u4E3A\u65E7\u522B\u540D\u4ECD\u53EF\u7528\uFF09\uFF08\u6536\u5230 ${JSON.stringify(next)}\uFF09`);
273
285
  }
274
- theme = next;
275
- if (panel) void panel.setTheme(next);
286
+ theme = normalizePanelTheme(next);
287
+ if (panel) void panel.setTheme(theme);
276
288
  else render();
277
289
  }
278
290
  function onClick(ev) {
@@ -285,7 +297,6 @@ function mountUpdateEntry(container, options) {
285
297
  void activate();
286
298
  return;
287
299
  }
288
- if (panelMode === "dialog" && closest('[data-action="close-view"]')) close();
289
300
  } catch {
290
301
  }
291
302
  }
package/dist/host.d.ts CHANGED
@@ -10,8 +10,8 @@ export { COPY_BUDGET_CHARS, DETAIL_MAX_CHARS, DIAG_DROP_ORDER, DIAG_INLINE_BUDGE
10
10
  export type { DiagDropKey, RedactionRuleName } from './redaction.js';
11
11
  export { DIAG_ACTIONS, DIAG_KEYS, DIAG_KNOWN_TYPES, DIAG_STAGE_EXPECT, DIAG_STAGES, DIAG_VERSION, buildDiag, deriveAction, deriveRouteMethod, deriveStage, enforceBudget as enforceDiagBudgetForPhone, } from './diag.js';
12
12
  export type { DiagAction, DiagInput, DiagObject, DiagStage } from './diag.js';
13
- export { CHANGELOG_ALL_CATEGORIES, CHANGELOG_FILENAME, CHANGELOG_FOLDED, CHANGELOG_MAX_BULLETS_PER_SECTION, CHANGELOG_MAX_BULLET_CHARS, CHANGELOG_MAX_CHARS, CHANGELOG_MAX_ENTRIES, CHANGELOG_MUST_SHOW, CHANGELOG_NEUTRAL_HINT, CHANGELOG_NEUTRAL_LINE, changelogForUpdate, hasVisibleSections, isUnreleasedVersion, parseChangelog, renderChangelogHTML, renderChangelogNeutral, renderChangelogSection, selectChangelogEntries, } from './changelog.js';
14
- export type { ChangelogCategory, ChangelogEntry } from './changelog.js';
13
+ export { CHANGELOG_ALL_CATEGORIES, CHANGELOG_FILENAME, CHANGELOG_FOLDED, CHANGELOG_MAX_BULLETS_PER_SECTION, CHANGELOG_MAX_BULLET_CHARS, CHANGELOG_MAX_CHARS, CHANGELOG_MAX_ENTRIES, CHANGELOG_MUST_SHOW, CHANGELOG_NEUTRAL_HINT, CHANGELOG_NEUTRAL_LINE, BREAKING_BADGE_TEXT, CHANGELOG_POLL_BACKOFF_MS, changelogForUpdate, countsOf, hasVisibleSections, isBreakingChangelogItem, isUnreleasedVersion, parseChangelog, renderChangelogHTML, renderChangelogItem, renderChangelogNeutral, renderChangelogSection, selectChangelogEntries, shouldFetchChangelog, splitBreakingPrefix, truncatedNoteHTML, validateChangelog, yankedBannerHTML, } from './changelog.js';
14
+ export type { ChangelogCategory, ChangelogDiagnostic, ChangelogEntry, ChangelogValidation } from './changelog.js';
15
15
  export { CHANGELOG_DEFAULT_REGISTRY, CHANGELOG_FETCH_MAX_BYTES, CHANGELOG_TAR_MAX_BYTES, CHANGELOG_TARBALL_MAX_BYTES, extractChangelogFromTar, fetchReleaseChangelogText, readInstalledChangelogText, } from './changelog-io.js';
16
16
  export type { ReleaseChangelogRef } from './changelog-io.js';
17
17
  export { createUpdateDiskPorts, createUpdateExecutor, pathsForUpdate, resolveCliEntry } from './store.js';