dsh-plugin-update 0.2.0 → 0.3.0

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.
@@ -0,0 +1,258 @@
1
+ // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/changelog-io.ts
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { gunzipSync } from "node:zlib";
6
+ import { CHANGELOG_FILENAME, CHANGELOG_MAX_CHARS } from "./changelog.js";
7
+ import { INTEGRITY_PATTERN } from "./service.js";
8
+ const CHANGELOG_TARBALL_MAX_BYTES = 8 * 1024 * 1024;
9
+ const CHANGELOG_TAR_MAX_BYTES = 32 * 1024 * 1024;
10
+ const CHANGELOG_FETCH_MAX_BYTES = 64 * 1024;
11
+ const CHANGELOG_DEFAULT_REGISTRY = "https://registry.npmjs.org/";
12
+ function byteLengthOf(text) {
13
+ try {
14
+ return Buffer.byteLength(text, "utf8");
15
+ } catch {
16
+ return text.length;
17
+ }
18
+ }
19
+ function truncateToBytes(text, maxBytes) {
20
+ try {
21
+ if (byteLengthOf(text) <= maxBytes) return text;
22
+ const buf = Buffer.from(text, "utf8");
23
+ return buf.slice(0, maxBytes).toString();
24
+ } catch {
25
+ return text.slice(0, Math.min(text.length, maxBytes));
26
+ }
27
+ }
28
+ function timeoutSignal(ms) {
29
+ try {
30
+ const ctor = globalThis.AbortSignal;
31
+ if (ctor && typeof ctor.timeout === "function") return ctor.timeout(ms);
32
+ } catch {
33
+ }
34
+ return void 0;
35
+ }
36
+ async function readInstalledChangelogText(targetPackageDir, opts) {
37
+ try {
38
+ const dir = typeof targetPackageDir === "string" ? targetPackageDir.trim() : "";
39
+ if (!dir) return null;
40
+ const maxBytes = opts && typeof opts.maxBytes === "number" && Number.isFinite(opts.maxBytes) && opts.maxBytes > 0 ? Math.floor(opts.maxBytes) : CHANGELOG_FETCH_MAX_BYTES;
41
+ let text;
42
+ try {
43
+ text = await readFile(join(dir, CHANGELOG_FILENAME), "utf8");
44
+ } catch {
45
+ return null;
46
+ }
47
+ if (typeof text !== "string" || !text.trim()) return null;
48
+ if (text.length > CHANGELOG_MAX_CHARS) text = text.slice(0, CHANGELOG_MAX_CHARS);
49
+ if (byteLengthOf(text) > maxBytes) text = truncateToBytes(text, maxBytes);
50
+ return text.trim() ? text : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function readResponseBytes(response) {
56
+ try {
57
+ const r = response;
58
+ if (r && typeof r.arrayBuffer === "function") {
59
+ const ab = await r.arrayBuffer();
60
+ if (ab instanceof ArrayBuffer) return new Uint8Array(ab);
61
+ if (ab && typeof ab.byteLength === "number") {
62
+ return new Uint8Array(ab);
63
+ }
64
+ return null;
65
+ }
66
+ if (r && typeof r.bytes === "function") {
67
+ const b = await r.bytes();
68
+ if (b instanceof Uint8Array) return b;
69
+ return null;
70
+ }
71
+ return null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ function parseOctal(bytes, offset, length) {
77
+ try {
78
+ let s = "";
79
+ for (let i = 0; i < length; i++) {
80
+ const c = bytes[offset + i];
81
+ if (c === 0 || c === 32) {
82
+ if (s) break;
83
+ continue;
84
+ }
85
+ s += String.fromCharCode(c);
86
+ if (s.length > 12) break;
87
+ }
88
+ s = s.trim();
89
+ if (!/^[0-7]+$/.test(s)) return NaN;
90
+ return parseInt(s, 8);
91
+ } catch {
92
+ return NaN;
93
+ }
94
+ }
95
+ function readTarName(bytes, offset) {
96
+ try {
97
+ let end = offset;
98
+ while (end < offset + 100 && end < bytes.length && bytes[end] !== 0) end++;
99
+ const slice = bytes.slice(offset, end);
100
+ try {
101
+ return Buffer.from(slice).toString().trim();
102
+ } catch {
103
+ let s = "";
104
+ for (let i = 0; i < slice.length; i++) s += String.fromCharCode(slice[i]);
105
+ return s.trim();
106
+ }
107
+ } catch {
108
+ return "";
109
+ }
110
+ }
111
+ function normalizeTarEntryName(raw) {
112
+ try {
113
+ let n = String(raw || "").trim().replace(/\\/g, "/");
114
+ n = n.replace(/^\.\//, "");
115
+ n = n.replace(/^\/+/, "");
116
+ return n;
117
+ } catch {
118
+ return "";
119
+ }
120
+ }
121
+ function isChangelogEntryName(normalized) {
122
+ try {
123
+ if (!normalized) return false;
124
+ if (normalized === CHANGELOG_FILENAME) return true;
125
+ if (normalized.endsWith(`/${CHANGELOG_FILENAME}`)) return true;
126
+ return false;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ function extractChangelogFromTar(tarBytes, maxBytes = CHANGELOG_FETCH_MAX_BYTES) {
132
+ try {
133
+ if (!tarBytes || tarBytes.length < 512 || tarBytes.length % 512 !== 0) {
134
+ if (!tarBytes || tarBytes.length < 512) return null;
135
+ }
136
+ let offset = 0;
137
+ while (offset + 512 <= tarBytes.length) {
138
+ let allZero = true;
139
+ for (let i = 0; i < 512; i++) {
140
+ if (tarBytes[offset + i] !== 0) {
141
+ allZero = false;
142
+ break;
143
+ }
144
+ }
145
+ if (allZero) break;
146
+ const rawName = readTarName(tarBytes, offset);
147
+ const name = normalizeTarEntryName(rawName);
148
+ const size = parseOctal(tarBytes, offset + 124, 12);
149
+ if (!Number.isFinite(size) || size < 0) return null;
150
+ const typeflag = String.fromCharCode(tarBytes[offset + 156] ?? 0);
151
+ const dataStart = offset + 512;
152
+ const dataEnd = dataStart + size;
153
+ if (dataEnd > tarBytes.length) return null;
154
+ const isFile = typeflag === "0" || typeflag === "\0" || typeflag === "";
155
+ if (isFile && isChangelogEntryName(name)) {
156
+ const slice = tarBytes.slice(dataStart, dataEnd);
157
+ let text = "";
158
+ try {
159
+ text = Buffer.from(slice).toString();
160
+ } catch {
161
+ return null;
162
+ }
163
+ if (!text.trim()) return null;
164
+ if (text.length > CHANGELOG_MAX_CHARS) text = text.slice(0, CHANGELOG_MAX_CHARS);
165
+ if (byteLengthOf(text) > maxBytes) text = truncateToBytes(text, maxBytes);
166
+ return text.trim() ? text : null;
167
+ }
168
+ const blocks = Math.ceil(size / 512);
169
+ offset = dataStart + blocks * 512;
170
+ if (blocks < 0) return null;
171
+ }
172
+ return null;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ function verifyIntegrity(bytes, integrity) {
178
+ try {
179
+ const m = String(integrity).match(/^sha512-([A-Za-z0-9+/]{86}==)$/);
180
+ if (!m) return false;
181
+ const actual = createHash("sha512").update(Buffer.from(bytes)).digest("base64");
182
+ return actual === m[1];
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+ async function fetchReleaseChangelogText(fetchImpl, release, opts) {
188
+ try {
189
+ if (typeof fetchImpl !== "function") return null;
190
+ const tarballText = release?.tarball;
191
+ const integrityText = release?.integrity;
192
+ if (typeof tarballText !== "string" || !tarballText) return null;
193
+ if (typeof integrityText !== "string" || !new RegExp(INTEGRITY_PATTERN).test(integrityText)) return null;
194
+ const registryUrl = opts && typeof opts.registryUrl === "string" && opts.registryUrl ? String(opts.registryUrl) : CHANGELOG_DEFAULT_REGISTRY;
195
+ let tarballUrl;
196
+ let registryOrigin = "";
197
+ try {
198
+ tarballUrl = new URL(tarballText);
199
+ registryOrigin = new URL(registryUrl).origin;
200
+ } catch {
201
+ return null;
202
+ }
203
+ if (tarballUrl.protocol !== "https:") return null;
204
+ if (tarballUrl.origin !== registryOrigin) return null;
205
+ if (tarballUrl.username || tarballUrl.password || tarballUrl.search || tarballUrl.hash) return null;
206
+ if (!tarballUrl.pathname.endsWith(".tgz")) return null;
207
+ const targetName = opts && typeof opts.targetPackageName === "string" ? String(opts.targetPackageName) : "";
208
+ const versionText = release && typeof release.version === "string" ? String(release.version) : "";
209
+ if (targetName && versionText) {
210
+ const expectPath = `/${targetName}/-/${targetName}-${versionText}.tgz`;
211
+ if (tarballUrl.pathname !== expectPath) return null;
212
+ }
213
+ const timeoutMs = opts && typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? Math.floor(opts.timeoutMs) : 1e4;
214
+ const maxTarballBytes = opts && typeof opts.maxTarballBytes === "number" && Number.isFinite(opts.maxTarballBytes) && opts.maxTarballBytes > 0 ? Math.floor(opts.maxTarballBytes) : CHANGELOG_TARBALL_MAX_BYTES;
215
+ const maxChangelogBytes = opts && typeof opts.maxChangelogBytes === "number" && Number.isFinite(opts.maxChangelogBytes) && opts.maxChangelogBytes > 0 ? Math.floor(opts.maxChangelogBytes) : CHANGELOG_FETCH_MAX_BYTES;
216
+ let response = null;
217
+ try {
218
+ response = await fetchImpl(tarballUrl.href, {
219
+ headers: { accept: "application/octet-stream" },
220
+ redirect: "error",
221
+ signal: timeoutSignal(timeoutMs)
222
+ });
223
+ } catch {
224
+ return null;
225
+ }
226
+ const ok = response?.ok;
227
+ if (ok !== true) return null;
228
+ try {
229
+ const headers = response.headers;
230
+ const declared = headers && typeof headers.get === "function" ? Number(headers.get("content-length")) : NaN;
231
+ if (Number.isFinite(declared) && declared > maxTarballBytes) return null;
232
+ } catch {
233
+ }
234
+ const bytes = await readResponseBytes(response);
235
+ if (!bytes || bytes.length === 0 || bytes.length > maxTarballBytes) return null;
236
+ if (!verifyIntegrity(bytes, integrityText)) return null;
237
+ let tar;
238
+ try {
239
+ const out = gunzipSync(Buffer.from(bytes));
240
+ tar = new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
241
+ } catch {
242
+ return null;
243
+ }
244
+ if (tar.length === 0 || tar.length > CHANGELOG_TAR_MAX_BYTES) return null;
245
+ return extractChangelogFromTar(tar, maxChangelogBytes);
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ export {
251
+ CHANGELOG_DEFAULT_REGISTRY,
252
+ CHANGELOG_FETCH_MAX_BYTES,
253
+ CHANGELOG_TARBALL_MAX_BYTES,
254
+ CHANGELOG_TAR_MAX_BYTES,
255
+ extractChangelogFromTar,
256
+ fetchReleaseChangelogText,
257
+ readInstalledChangelogText
258
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * src/changelog.ts —— 包内 CHANGELOG.md 的纯解析与面板渲染(#23,承接 #11 取证)。
3
+ *
4
+ * 取证结论(research/changelog-optimal-form.md):
5
+ * 包内 `CHANGELOG.md`(包根 + files 白名单已落地)是唯一推荐数据源;
6
+ * Keep-a-Changelog 子集;缺日志中性提示、不挡安装。
7
+ *
8
+ * 本模块零依赖、纯函数、无 Node/浏览器专属能力,两边都跑得动:
9
+ * 解析只做字符串切分,不读盘、不联网;版本语义与核心同口径
10
+ * (validReleaseVersion / compareReleaseVersions,stable 纯三段、prerelease 显式)。
11
+ *
12
+ * 面板约定(Question 原文逐字落实):
13
+ * - `Added/Fixed/Changed` 必显;`Deprecated/Removed/Security` 透传折叠;
14
+ * - `Unreleased` 与空节忽略;
15
+ * - 缺日志中性提示,不挡安装、不写 blockedReason、不动快照六字段
16
+ * (本模块不碰快照,面板只增一节 HTML,安装门控只跟快照);
17
+ * - 已装版离线读、新版按需取 tarball 同名文件复用官方源 integrity
18
+ * (I/O 在 `src/changelog-io.ts` Node 侧,取不到回落中性提示);
19
+ * - 零依赖纯函数解析器(本文件零导入)。
20
+ */
21
+ /** 包内日志文件名(包根,与 tarball 内同名文件同一名字)。 */
22
+ export declare const CHANGELOG_FILENAME = "CHANGELOG.md";
23
+ /** 缺日志时的中性提示(不挡安装,安装按钮状态不变)。 */
24
+ export declare const CHANGELOG_NEUTRAL_HINT = "\u4F5C\u8005\u672A\u63D0\u4F9B\u66F4\u65B0\u8BF4\u660E";
25
+ /** 中性提示完整行(含“安装不受影响”,把“不挡安装”说成人话)。 */
26
+ export declare const CHANGELOG_NEUTRAL_LINE = "\u4F5C\u8005\u672A\u63D0\u4F9B\u66F4\u65B0\u8BF4\u660E\uFF0C\u5B89\u88C5\u4E0D\u53D7\u5F71\u54CD\u3002";
27
+ /** 输入上限(字符):超限截断后解析,永不抛错、不OOM。 */
28
+ export declare const CHANGELOG_MAX_CHARS: number;
29
+ /** 版本节上限:超限只取文件顺序前 N 节(Keep-a-Changelog 最新在前)。 */
30
+ export declare const CHANGELOG_MAX_ENTRIES = 100;
31
+ /** 每类条目上限:超限只取前 N 条。 */
32
+ export declare const CHANGELOG_MAX_BULLETS_PER_SECTION = 200;
33
+ /** 单条上限(字符):超限在尾部截断并加省略号。 */
34
+ export declare const CHANGELOG_MAX_BULLET_CHARS = 500;
35
+ /** 六类全集(Keep-a-Changelog #types 节)。 */
36
+ export declare const CHANGELOG_ALL_CATEGORIES: readonly ["Added", "Fixed", "Changed", "Deprecated", "Removed", "Security"];
37
+ export type ChangelogCategory = (typeof CHANGELOG_ALL_CATEGORIES)[number];
38
+ /** 必显三类(面板展开渲染)。 */
39
+ export declare const CHANGELOG_MUST_SHOW: readonly ChangelogCategory[];
40
+ /** 透传折叠三类(原样展示、不解析、不参与“有无新版”判断)。 */
41
+ export declare const CHANGELOG_FOLDED: readonly ChangelogCategory[];
42
+ /** 六类中文名(面板双语标题用,顺序与英文一一对应)。 */
43
+ export declare const CHANGELOG_CATEGORY_ZH: Record<ChangelogCategory, string>;
44
+ /** 一节版本(Unreleased 也保留在解析结果里,渲染与区间筛选时忽略)。 */
45
+ export interface ChangelogEntry {
46
+ /** 归一化版本号:`Unreleased` 或发行版本号(如 `1.2.3`、`1.2.3-rc.1`)。 */
47
+ version: string;
48
+ /** 标题里的 ISO 日期(`YYYY-MM-DD`),没有为 null。 */
49
+ date: string | null;
50
+ /**
51
+ * 标题带 `[YANKED]` 标记(仅文本提示,不做机器信号:区间照算,渲染补“已撤回”)。
52
+ * 对抗复查补记:Keep-a-Changelog 要求 yanked 可见,本模块此前直接丢掉标记。
53
+ */
54
+ yanked: boolean;
55
+ /** 六类各自的条目(空类为空数组,空节忽略由渲染层执行)。 */
56
+ sections: Record<ChangelogCategory, string[]>;
57
+ }
58
+ /** 是否 Unreleased(大小写不敏感,前后空白忽略)。 */
59
+ export declare function isUnreleasedVersion(v: unknown): boolean;
60
+ /**
61
+ * 解析 CHANGELOG Markdown(纯函数,永不抛错):
62
+ * 非字符串/空串回 [];超长截断后解析;未知版本标题与其下条目一律丢弃;
63
+ * 全空节版本直接丢弃(空节忽略);Unreleased 保留(渲染与区间时忽略)。
64
+ */
65
+ export declare function parseChangelog(markdown: unknown): ChangelogEntry[];
66
+ /** 该节是否有可见内容(六类任一非空)。 */
67
+ export declare function hasVisibleSections(entry: ChangelogEntry | null | undefined): boolean;
68
+ /**
69
+ * 按区间筛选(文件顺序不变,最新在前):
70
+ * 只收发行版本号(Unreleased 与非法版本一律忽略);
71
+ * 只收 `(fromExclusive, toInclusive]`;
72
+ * from 非法/缺省视为 -inf(全收 `<= to`);to 非法/缺省回 []。
73
+ *
74
+ * 预发布节取舍(#23 对抗复查定稿):区间内的预发布节一并收录(如 `(1.0.0, 1.1.0]` 含 `1.1.0-rc.1`)。
75
+ * 通道门禁不在此做——`to` 本身由核心按通道滤过(stable 通道下远端永不为预发布),
76
+ * 此处只管“两版之间有何内容”的人读完整性;面板永不拿它做安装分支。
77
+ */
78
+ export declare function selectChangelogEntries(entries: unknown, fromExclusive: unknown, toInclusive: unknown): ChangelogEntry[];
79
+ /**
80
+ * 更新区间便捷口:from 取运行版( fallback 已装版),to 取远端版;
81
+ * 任一缺失/非法即回 [](调用方渲染中性提示,不挡安装)。
82
+ */
83
+ export declare function changelogForUpdate(entries: unknown, runningVersion: unknown, latestVersion: unknown, installedVersion?: unknown): ChangelogEntry[];
84
+ /** 单节 HTML(调用方保证已过滤 Unreleased 与空节,本函数再兜底一次)。 */
85
+ export declare function renderChangelogSection(entry: ChangelogEntry): string;
86
+ /** 中性提示 HTML(缺日志、取不到、无区间内容时统一用它,不挡安装)。 */
87
+ export declare function renderChangelogNeutral(): string;
88
+ /**
89
+ * 区间渲染 HTML(纯函数):
90
+ * entries 非数组/空、无可见区间内容一律回中性提示;
91
+ * Unreleased 与空节永不渲染;输出已转义,可直接拼进面板。
92
+ */
93
+ export declare function renderChangelogHTML(entries: unknown, opts?: {
94
+ from?: string | null;
95
+ to?: string | null;
96
+ }): string;
@@ -0,0 +1,287 @@
1
+ // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/changelog.ts
2
+ import { compareReleaseVersions, validReleaseVersion } from "./service.js";
3
+ const CHANGELOG_FILENAME = "CHANGELOG.md";
4
+ const CHANGELOG_NEUTRAL_HINT = "\u4F5C\u8005\u672A\u63D0\u4F9B\u66F4\u65B0\u8BF4\u660E";
5
+ const CHANGELOG_NEUTRAL_LINE = "\u4F5C\u8005\u672A\u63D0\u4F9B\u66F4\u65B0\u8BF4\u660E\uFF0C\u5B89\u88C5\u4E0D\u53D7\u5F71\u54CD\u3002";
6
+ const CHANGELOG_MAX_CHARS = 64 * 1024;
7
+ const CHANGELOG_MAX_ENTRIES = 100;
8
+ const CHANGELOG_MAX_BULLETS_PER_SECTION = 200;
9
+ const CHANGELOG_MAX_BULLET_CHARS = 500;
10
+ const CHANGELOG_ALL_CATEGORIES = [
11
+ "Added",
12
+ "Fixed",
13
+ "Changed",
14
+ "Deprecated",
15
+ "Removed",
16
+ "Security"
17
+ ];
18
+ const CHANGELOG_MUST_SHOW = ["Added", "Fixed", "Changed"];
19
+ const CHANGELOG_FOLDED = ["Deprecated", "Removed", "Security"];
20
+ const CHANGELOG_CATEGORY_ZH = {
21
+ Added: "\u65B0\u589E",
22
+ Fixed: "\u4FEE\u590D",
23
+ Changed: "\u53D8\u66F4",
24
+ Deprecated: "\u5F03\u7528\u9884\u544A",
25
+ Removed: "\u79FB\u9664",
26
+ Security: "\u5B89\u5168"
27
+ };
28
+ function emptySections() {
29
+ return { Added: [], Fixed: [], Changed: [], Deprecated: [], Removed: [], Security: [] };
30
+ }
31
+ function isCategoryName(v) {
32
+ return CHANGELOG_ALL_CATEGORIES.includes(v);
33
+ }
34
+ function normalizeCategory(raw) {
35
+ const t = String(raw || "").trim().toLowerCase();
36
+ if (t === "added") return "Added";
37
+ if (t === "fixed") return "Fixed";
38
+ if (t === "changed") return "Changed";
39
+ if (t === "deprecated") return "Deprecated";
40
+ if (t === "removed") return "Removed";
41
+ if (t === "security") return "Security";
42
+ return null;
43
+ }
44
+ function isUnreleasedVersion(v) {
45
+ return typeof v === "string" && v.trim().toLowerCase() === "unreleased";
46
+ }
47
+ function extractVersionDateAndYanked(title) {
48
+ const t = String(title || "").trim();
49
+ if (!t) return { version: null, date: null, yanked: false };
50
+ const yanked = /\[YANKED\]/i.test(t);
51
+ if (/unreleased/i.test(t)) return { version: "Unreleased", date: null, yanked };
52
+ const vm = t.match(/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
53
+ if (!vm) return { version: null, date: null, yanked: false };
54
+ const dm = t.match(/(\d{4}-\d{2}-\d{2})/);
55
+ return { version: vm[1], date: dm ? dm[1] : null, yanked };
56
+ }
57
+ function truncateBullet(text) {
58
+ const t = String(text || "").replace(/\s+/g, " ").trim();
59
+ if (!t) return "";
60
+ if (t.length <= CHANGELOG_MAX_BULLET_CHARS) return t;
61
+ return `${t.slice(0, CHANGELOG_MAX_BULLET_CHARS - 1)}\u2026`;
62
+ }
63
+ function parseChangelog(markdown) {
64
+ try {
65
+ if (typeof markdown !== "string" || !markdown.trim()) return [];
66
+ let text = markdown.replace(/\r\n/g, "\n");
67
+ if (text.length > CHANGELOG_MAX_CHARS) text = text.slice(0, CHANGELOG_MAX_CHARS);
68
+ const lines = text.split("\n");
69
+ const entries = [];
70
+ let cur = null;
71
+ let curCat = null;
72
+ let inFence = false;
73
+ const pushCurrent = () => {
74
+ if (!cur) return;
75
+ const hasAny = CHANGELOG_ALL_CATEGORIES.some(
76
+ (c) => cur.sections[c].length > 0
77
+ );
78
+ if (!hasAny) return;
79
+ for (const c of CHANGELOG_ALL_CATEGORIES) {
80
+ const list = cur.sections[c];
81
+ if (list.length > CHANGELOG_MAX_BULLETS_PER_SECTION) {
82
+ cur.sections[c] = list.slice(0, CHANGELOG_MAX_BULLETS_PER_SECTION);
83
+ }
84
+ }
85
+ entries.push(cur);
86
+ };
87
+ for (const rawLine of lines) {
88
+ const line = String(rawLine ?? "");
89
+ if (/^\s*(```|~~~)/.test(line)) {
90
+ inFence = !inFence;
91
+ continue;
92
+ }
93
+ if (inFence) continue;
94
+ const versionHeading = line.match(/^##(?!#)\s*(.+?)\s*$/);
95
+ if (versionHeading) {
96
+ pushCurrent();
97
+ cur = null;
98
+ curCat = null;
99
+ const title = (versionHeading[1] ?? "").trim();
100
+ const { version, date, yanked } = extractVersionDateAndYanked(title);
101
+ if (!version) continue;
102
+ cur = { version, date, yanked, sections: emptySections() };
103
+ continue;
104
+ }
105
+ const catMatch = line.match(/^###\s*(.+?)\s*$/);
106
+ if (catMatch) {
107
+ const cat = normalizeCategory(catMatch[1] ?? "");
108
+ curCat = cur && cat && isCategoryName(cat) ? cat : null;
109
+ continue;
110
+ }
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);
115
+ continue;
116
+ }
117
+ const trimmed = line.trim();
118
+ if (trimmed && !trimmed.startsWith("#") && /^\s/.test(line) && cur && curCat) {
119
+ const list = cur.sections[curCat];
120
+ if (list.length > 0) {
121
+ const merged = truncateBullet(`${list[list.length - 1]} ${trimmed}`);
122
+ if (merged) list[list.length - 1] = merged;
123
+ }
124
+ continue;
125
+ }
126
+ }
127
+ pushCurrent();
128
+ return entries.slice(0, CHANGELOG_MAX_ENTRIES);
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+ function hasVisibleSections(entry) {
134
+ if (!entry || typeof entry !== "object") return false;
135
+ try {
136
+ return CHANGELOG_ALL_CATEGORIES.some((c) => Array.isArray(entry.sections?.[c]) && entry.sections[c].length > 0);
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+ function compareReleaseSafe(a, b) {
142
+ try {
143
+ return compareReleaseVersions(a, b);
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ function selectChangelogEntries(entries, fromExclusive, toInclusive) {
149
+ try {
150
+ if (!Array.isArray(entries) || entries.length === 0) return [];
151
+ const to = typeof toInclusive === "string" ? toInclusive.trim() : "";
152
+ if (!to || !validReleaseVersion(to)) return [];
153
+ const from = typeof fromExclusive === "string" ? fromExclusive.trim() : "";
154
+ const fromValid = from && validReleaseVersion(from) ? from : null;
155
+ const out = [];
156
+ for (const e of entries) {
157
+ if (!e || typeof e !== "object") continue;
158
+ const v = typeof e.version === "string" ? String(e.version) : "";
159
+ if (!v || isUnreleasedVersion(v) || !validReleaseVersion(v)) continue;
160
+ if (!hasVisibleSections(e)) continue;
161
+ const leTo = compareReleaseSafe(v, to);
162
+ if (leTo === null || leTo > 0) continue;
163
+ if (fromValid) {
164
+ const gtFrom = compareReleaseSafe(v, fromValid);
165
+ if (gtFrom === null || gtFrom <= 0) continue;
166
+ }
167
+ out.push(e);
168
+ }
169
+ return out;
170
+ } catch {
171
+ return [];
172
+ }
173
+ }
174
+ function changelogForUpdate(entries, runningVersion, latestVersion, installedVersion) {
175
+ try {
176
+ const to = typeof latestVersion === "string" ? latestVersion.trim() : "";
177
+ if (!to || !validReleaseVersion(to)) return [];
178
+ const run = typeof runningVersion === "string" ? runningVersion.trim() : "";
179
+ const inst = typeof installedVersion === "string" ? installedVersion.trim() : "";
180
+ const from = run && validReleaseVersion(run) ? run : inst && validReleaseVersion(inst) ? inst : "";
181
+ if (!from) return [];
182
+ const order = compareReleaseSafe(to, from);
183
+ if (order === null || order <= 0) return [];
184
+ return selectChangelogEntries(entries, from, to);
185
+ } catch {
186
+ return [];
187
+ }
188
+ }
189
+ function escapeChangelogHtml(text) {
190
+ return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
191
+ }
192
+ function renderChangelogSection(entry) {
193
+ try {
194
+ if (!entry || typeof entry !== "object") return "";
195
+ const version = String(entry.version ?? "").trim();
196
+ if (!version || isUnreleasedVersion(version)) return "";
197
+ 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;
201
+ 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>`);
204
+ 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>`);
210
+ parts.push("<ul>");
211
+ for (const item of items) {
212
+ if (!item) continue;
213
+ parts.push(`<li>${escapeChangelogHtml(item)}</li>`);
214
+ }
215
+ parts.push("</ul></div>");
216
+ }
217
+ 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>`);
223
+ parts.push("<ul>");
224
+ for (const item of items) {
225
+ if (!item) continue;
226
+ parts.push(`<li>${escapeChangelogHtml(item)}</li>`);
227
+ }
228
+ parts.push("</ul></details>");
229
+ }
230
+ parts.push("</div>");
231
+ return parts.join("\n");
232
+ } catch {
233
+ return "";
234
+ }
235
+ }
236
+ function renderChangelogNeutral() {
237
+ return `<div class="dsh-upd-changelog-neutral">${escapeChangelogHtml(CHANGELOG_NEUTRAL_LINE)}</div>`;
238
+ }
239
+ function renderChangelogHTML(entries, opts) {
240
+ try {
241
+ if (!Array.isArray(entries) || entries.length === 0) return renderChangelogNeutral();
242
+ const from = opts && typeof opts.from === "string" ? opts.from : null;
243
+ const to = opts && typeof opts.to === "string" ? opts.to : null;
244
+ let ranged;
245
+ if (from !== null || to !== null) {
246
+ ranged = selectChangelogEntries(entries, from, to);
247
+ } else {
248
+ ranged = entries.filter((e) => {
249
+ try {
250
+ const v = String(e.version ?? "");
251
+ return !!v && !isUnreleasedVersion(v) && hasVisibleSections(e);
252
+ } catch {
253
+ return false;
254
+ }
255
+ });
256
+ }
257
+ if (ranged.length === 0) return renderChangelogNeutral();
258
+ const blocks = ranged.map((e) => renderChangelogSection(e)).filter((s) => !!s);
259
+ if (blocks.length === 0) return renderChangelogNeutral();
260
+ return `<div class="dsh-upd-changelog">
261
+ ${blocks.join("\n")}
262
+ </div>`;
263
+ } catch {
264
+ return renderChangelogNeutral();
265
+ }
266
+ }
267
+ export {
268
+ CHANGELOG_ALL_CATEGORIES,
269
+ CHANGELOG_CATEGORY_ZH,
270
+ CHANGELOG_FILENAME,
271
+ CHANGELOG_FOLDED,
272
+ CHANGELOG_MAX_BULLETS_PER_SECTION,
273
+ CHANGELOG_MAX_BULLET_CHARS,
274
+ CHANGELOG_MAX_CHARS,
275
+ CHANGELOG_MAX_ENTRIES,
276
+ CHANGELOG_MUST_SHOW,
277
+ CHANGELOG_NEUTRAL_HINT,
278
+ CHANGELOG_NEUTRAL_LINE,
279
+ changelogForUpdate,
280
+ hasVisibleSections,
281
+ isUnreleasedVersion,
282
+ parseChangelog,
283
+ renderChangelogHTML,
284
+ renderChangelogNeutral,
285
+ renderChangelogSection,
286
+ selectChangelogEntries
287
+ };
@@ -0,0 +1,16 @@
1
+ import { type PhoneAction } from './config.js';
2
+ export { buildPhoneName, buildPhoneNames } from './config.js';
3
+ export type { PhoneAction } from './config.js';
4
+ export { manualCommand } from './commands.js';
5
+ export { QUEUE_INTENT_TTL_MS, cancelEnqueuedInQueue, emptyQueueState, enqueueInQueue, isHeadOfQueue, isQueueBusy, normalizeQueueState, pruneExpiredIntents, queuePositionOf, releaseOwnerInQueue, setOwnerIfFree, visibleQueueFor, } from './queue.js';
6
+ export type { QueuedEntry, QueueOwner, UpdateQueueState, VisibleQueue, VisibleQueueOwner } from './queue.js';
7
+ export { BATCH_SESSION_VERSION, batchEntryOf, batchProgress, batchRequestId, createBatchSession, emptyBatchSession, failedKeys, isBatchFinished, isTerminalPhase, markBatchEntry, needsRestartKeys, nextBatchKey, normalizeBatchSession, orderTargets, resumeBatchSession, } from './batch.js';
8
+ export type { BatchEntry, BatchPhase, BatchProgress, BatchSession } from './batch.js';
9
+ export { CHANGELOG_FILENAME, CHANGELOG_NEUTRAL_HINT, CHANGELOG_NEUTRAL_LINE, changelogForUpdate, parseChangelog, renderChangelogHTML, selectChangelogEntries, } from './changelog.js';
10
+ export type { ChangelogCategory, ChangelogEntry } from './changelog.js';
11
+ export declare const CLIENT_POLL: {
12
+ readonly defaultMs: 1000;
13
+ readonly minMs: 250;
14
+ };
15
+ export declare function buildClientPhoneNames(prefix: string): Record<PhoneAction, string>;
16
+ export declare function assertPollInterval(ms: unknown): number;