dsh-version-check 0.0.0-stage → 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,29 @@
1
+ import { ChildProcess } from "node:child_process";
2
+ /** npm 安装结束时的结果。 */
3
+ export interface InstallResult {
4
+ code: number | null;
5
+ signal: string | null;
6
+ outputTail: string;
7
+ timedOut: boolean;
8
+ error?: Error;
9
+ }
10
+ /** npm 安装的参数与结束回调。 */
11
+ export interface InstallOptions {
12
+ npmExecutable: string;
13
+ npmPrefix: string;
14
+ version: string;
15
+ timeoutMs: number;
16
+ onExit: (result: InstallResult) => void;
17
+ }
18
+ /**
19
+ * 用 npm 全局安装指定版本的 `@deepseek-ai/dsh`。
20
+ * 输出收集末尾 4000 字符,超时后终止子进程。
21
+ */
22
+ export declare function runNpmInstall({ npmExecutable, npmPrefix, version, timeoutMs, onExit }: InstallOptions): ChildProcess;
23
+ /**
24
+ * 请求 systemd 重启 DSH 服务,使用 --no-block 立即返回。
25
+ * 监听 systemctl 的非零退出码,重启请求未被 systemd 接受时通过 onError 上报。
26
+ * @param unitName systemd 服务单元名。
27
+ * @param onError 失败时的回调;未提供时写入 stderr。
28
+ */
29
+ export declare function scheduleServiceRestart(unitName: string, onError?: ((error: Error) => void) | null): ChildProcess;
package/lib/install.js ADDED
@@ -0,0 +1,109 @@
1
+ import { spawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ // src/install.ts
4
+ /**
5
+ * npm 全局安装与 systemd 服务重启的子进程封装。
6
+ */
7
+ /**
8
+ * 用 npm 全局安装指定版本的 `@deepseek-ai/dsh`。
9
+ * 输出收集末尾 4000 字符,超时后终止子进程。
10
+ */
11
+ function runNpmInstall({ npmExecutable, npmPrefix, version, timeoutMs, onExit }) {
12
+ const child = spawn(npmExecutable, [
13
+ "install",
14
+ "-g",
15
+ `@deepseek-ai/dsh@${version}`,
16
+ "--prefix",
17
+ npmPrefix,
18
+ "--no-audit",
19
+ "--no-fund",
20
+ "--loglevel=error"
21
+ ], {
22
+ cwd: homedir(),
23
+ stdio: [
24
+ "ignore",
25
+ "pipe",
26
+ "pipe"
27
+ ]
28
+ });
29
+ let outputTail = "";
30
+ const append = (chunk) => {
31
+ outputTail = (outputTail + chunk).slice(-4e3);
32
+ };
33
+ child.stdout.setEncoding("utf8");
34
+ child.stderr.setEncoding("utf8");
35
+ child.stdout.on("data", append);
36
+ child.stderr.on("data", append);
37
+ let timedOut = false;
38
+ let settled = false;
39
+ let timer = null;
40
+ const finish = (result) => {
41
+ if (settled) return;
42
+ settled = true;
43
+ if (timer !== null) clearTimeout(timer);
44
+ onExit({
45
+ ...result,
46
+ outputTail
47
+ });
48
+ };
49
+ timer = setTimeout(() => {
50
+ timedOut = true;
51
+ child.kill("SIGKILL");
52
+ }, timeoutMs);
53
+ child.on("error", (error) => finish({
54
+ code: null,
55
+ signal: null,
56
+ timedOut: false,
57
+ error
58
+ }));
59
+ child.on("close", (code, signal) => finish({
60
+ code,
61
+ signal,
62
+ timedOut
63
+ }));
64
+ return child;
65
+ }
66
+ /**
67
+ * 请求 systemd 重启 DSH 服务,使用 --no-block 立即返回。
68
+ * 监听 systemctl 的非零退出码,重启请求未被 systemd 接受时通过 onError 上报。
69
+ * @param unitName systemd 服务单元名。
70
+ * @param onError 失败时的回调;未提供时写入 stderr。
71
+ */
72
+ function scheduleServiceRestart(unitName, onError = null) {
73
+ const child = spawn("systemctl", [
74
+ "--user",
75
+ "restart",
76
+ "--no-block",
77
+ unitName
78
+ ], {
79
+ detached: true,
80
+ stdio: [
81
+ "ignore",
82
+ "ignore",
83
+ "pipe"
84
+ ]
85
+ });
86
+ let stderrTail = "";
87
+ child.stderr.setEncoding("utf8");
88
+ child.stderr.on("data", (chunk) => {
89
+ stderrTail = (stderrTail + chunk).slice(-400);
90
+ });
91
+ let reported = false;
92
+ const report = (error) => {
93
+ if (reported) return;
94
+ reported = true;
95
+ if (typeof onError === "function") onError(error);
96
+ else console.error(`无法重启 DSH 服务: ${error.message}`);
97
+ };
98
+ child.on("error", (error) => report(error));
99
+ child.on("exit", (code, signal) => {
100
+ if (code === 0) return;
101
+ const detail = stderrTail.trim();
102
+ const suffix = detail.length > 0 ? `: ${detail}` : "";
103
+ const reason = signal === null ? `退出码 ${code}` : `信号 ${signal}`;
104
+ report(/* @__PURE__ */ new Error(`systemctl 重启请求失败(${reason})${suffix}`));
105
+ });
106
+ child.unref();
107
+ return child;
108
+ }
109
+ export { runNpmInstall, scheduleServiceRestart };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 用户设置:更新检查开关、自动检查间隔与发布渠道的校验、合并与持久化。
3
+ * 设置保存在 $DSH_HOME/dsh-version-check/preferences.json;
4
+ * profile patch 里的同名字段作为默认值,用户设置覆盖它们。
5
+ */
6
+ /** 生效的设置。 */
7
+ export interface Preferences {
8
+ checkEnabled: boolean;
9
+ checkIntervalMinutes: number;
10
+ channel: string;
11
+ }
12
+ /** 只包含用户提交字段的设置补丁。 */
13
+ export type PreferencesPatch = Partial<Preferences>;
14
+ /**
15
+ * 设置文件的绝对路径。
16
+ * @param env 环境变量,默认使用 process.env。
17
+ */
18
+ export declare function preferencesFilePath(env?: NodeJS.ProcessEnv): string;
19
+ /**
20
+ * 校验并归一化设置补丁,只保留提供的字段。
21
+ * @param raw 请求体。
22
+ */
23
+ export declare function normalizePreferencesPatch(raw: unknown): PreferencesPatch;
24
+ /**
25
+ * 合并默认设置与用户设置,返回实际生效的设置。
26
+ * @param defaults patch 层默认值。
27
+ * @param stored 已保存的用户设置。
28
+ */
29
+ export declare function resolvePreferences(defaults: Preferences, stored: unknown): Preferences;
30
+ /**
31
+ * 读取设置文件;文件不存在时返回空对象。
32
+ * 文件内容无法解析时抛错,由调用方按显式错误处理。
33
+ * @param path 设置文件路径。
34
+ */
35
+ export declare function readPreferencesFile(path: string): Record<string, unknown>;
36
+ /**
37
+ * 原子写入设置文件:先写临时文件再改名,权限为 0600。
38
+ * @param path 设置文件路径。
39
+ * @param preferences 生效设置。
40
+ */
41
+ export declare function writePreferencesFile(path: string, preferences: Preferences): void;
@@ -0,0 +1,132 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ // src/preferences.ts
5
+ /**
6
+ * 用户设置:更新检查开关、自动检查间隔与发布渠道的校验、合并与持久化。
7
+ * 设置保存在 $DSH_HOME/dsh-version-check/preferences.json;
8
+ * profile patch 里的同名字段作为默认值,用户设置覆盖它们。
9
+ */
10
+ const PREFERENCES_VERSION = 1;
11
+ const INTERVAL_MINUTES_MIN = 1;
12
+ const INTERVAL_MINUTES_MAX = 10080;
13
+ const CHANNEL_PATTERN = /^[A-Za-z][A-Za-z0-9._-]*$/;
14
+ const PREFERENCES_DIRECTORY = "dsh-version-check";
15
+ const PREFERENCES_FILENAME = "preferences.json";
16
+ const SETTING_FIELDS = [
17
+ "checkEnabled",
18
+ "checkIntervalMinutes",
19
+ "channel"
20
+ ];
21
+ /**
22
+ * 设置文件的绝对路径。
23
+ * @param env 环境变量,默认使用 process.env。
24
+ */
25
+ function preferencesFilePath(env = process.env) {
26
+ const home = typeof env.DSH_HOME === "string" && env.DSH_HOME.length > 0 ? env.DSH_HOME : join(homedir(), ".dsh");
27
+ return join(home, PREFERENCES_DIRECTORY, PREFERENCES_FILENAME);
28
+ }
29
+ function normalizeField(field, value) {
30
+ if (field === "checkEnabled") {
31
+ if (typeof value !== "boolean") throw new TypeError("checkEnabled 必须是布尔值");
32
+ return value;
33
+ }
34
+ if (field === "checkIntervalMinutes") {
35
+ if (typeof value !== "number" || !Number.isInteger(value) || value < INTERVAL_MINUTES_MIN || value > INTERVAL_MINUTES_MAX) throw new TypeError(`checkIntervalMinutes 必须是 ${INTERVAL_MINUTES_MIN} 到 ${INTERVAL_MINUTES_MAX} 之间的整数`);
36
+ return value;
37
+ }
38
+ if (field === "channel") {
39
+ if (typeof value !== "string") throw new TypeError("channel 必须是字符串");
40
+ const text = value.trim();
41
+ if (text !== "auto" && !CHANNEL_PATTERN.test(text)) throw new TypeError("channel 必须是 auto 或一个 npm dist-tag 名称");
42
+ return text;
43
+ }
44
+ throw new TypeError(`未知的设置字段: ${field}`);
45
+ }
46
+ /**
47
+ * 校验并归一化设置补丁,只保留提供的字段。
48
+ * @param raw 请求体。
49
+ */
50
+ function normalizePreferencesPatch(raw) {
51
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new TypeError("设置必须是 JSON 对象");
52
+ const patch = {};
53
+ for (const [field, value] of Object.entries(raw)) {
54
+ if (!isSettingField(field)) throw new TypeError(`未知的设置字段: ${field}`);
55
+ switch (field) {
56
+ case "checkEnabled":
57
+ patch.checkEnabled = normalizeField("checkEnabled", value);
58
+ break;
59
+ case "checkIntervalMinutes":
60
+ patch.checkIntervalMinutes = normalizeField("checkIntervalMinutes", value);
61
+ break;
62
+ case "channel": patch.channel = normalizeField("channel", value);
63
+ }
64
+ }
65
+ if (Object.keys(patch).length === 0) throw new TypeError("设置补丁不能为空");
66
+ return patch;
67
+ }
68
+ /**
69
+ * 合并默认设置与用户设置,返回实际生效的设置。
70
+ * @param defaults patch 层默认值。
71
+ * @param stored 已保存的用户设置。
72
+ */
73
+ function resolvePreferences(defaults, stored) {
74
+ const resolved = {
75
+ checkEnabled: normalizeField("checkEnabled", defaults.checkEnabled),
76
+ checkIntervalMinutes: normalizeField("checkIntervalMinutes", defaults.checkIntervalMinutes),
77
+ channel: normalizeField("channel", defaults.channel)
78
+ };
79
+ if (stored === null || typeof stored !== "object" || Array.isArray(stored)) throw new TypeError("已保存的设置必须是对象");
80
+ const storedFields = stored;
81
+ for (const field of SETTING_FIELDS) {
82
+ if (!Object.hasOwn(stored, field)) continue;
83
+ switch (field) {
84
+ case "checkEnabled":
85
+ resolved.checkEnabled = normalizeField("checkEnabled", storedFields[field]);
86
+ break;
87
+ case "checkIntervalMinutes":
88
+ resolved.checkIntervalMinutes = normalizeField("checkIntervalMinutes", storedFields[field]);
89
+ break;
90
+ case "channel": resolved.channel = normalizeField("channel", storedFields[field]);
91
+ }
92
+ }
93
+ return resolved;
94
+ }
95
+ /**
96
+ * 读取设置文件;文件不存在时返回空对象。
97
+ * 文件内容无法解析时抛错,由调用方按显式错误处理。
98
+ * @param path 设置文件路径。
99
+ */
100
+ function readPreferencesFile(path) {
101
+ if (!existsSync(path)) return {};
102
+ const text = readFileSync(path, "utf8");
103
+ const parsed = JSON.parse(text);
104
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`设置文件不是对象: ${path}`);
105
+ return parsed;
106
+ }
107
+ /**
108
+ * 原子写入设置文件:先写临时文件再改名,权限为 0600。
109
+ * @param path 设置文件路径。
110
+ * @param preferences 生效设置。
111
+ */
112
+ function writePreferencesFile(path, preferences) {
113
+ const directory = dirname(path);
114
+ mkdirSync(directory, { recursive: true });
115
+ const temporary = `${path}.${process.pid}.tmp`;
116
+ const payload = JSON.stringify({
117
+ version: PREFERENCES_VERSION,
118
+ ...preferences
119
+ }, null, 2) + "\n";
120
+ try {
121
+ writeFileSync(temporary, payload, { mode: 384 });
122
+ renameSync(temporary, path);
123
+ } finally {
124
+ rmSync(temporary, { force: true });
125
+ }
126
+ chmodSync(path, 384);
127
+ }
128
+ /** 判断字符串是否是设置字段名。 */
129
+ function isSettingField(field) {
130
+ return SETTING_FIELDS.includes(field);
131
+ }
132
+ export { normalizePreferencesPatch, preferencesFilePath, readPreferencesFile, resolvePreferences, writePreferencesFile };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * npm registry 的 dist-tags 查询、已发布版本列表与回滚候选筛选。
3
+ */
4
+ /** fetch 返回值的结构,只用到状态码与 JSON 解析。 */
5
+ export interface FetchResponseLike {
6
+ status: number;
7
+ json(): Promise<unknown>;
8
+ }
9
+ /** 测试可注入的 fetch 实现。 */
10
+ export type FetchLike = (url: string, init: {
11
+ headers: Record<string, string>;
12
+ signal: AbortSignal;
13
+ }) => Promise<FetchResponseLike>;
14
+ /** dist-tag 到版本号的映射。 */
15
+ export type DistTags = Record<string, string>;
16
+ /**
17
+ * 拉取 `@deepseek-ai/dsh` 的 dist-tags。
18
+ * @param registryUrl registry 地址,末尾斜杠可有可无。
19
+ * @param fetchImpl fetch 实现,测试时注入。
20
+ * @returns 只保留版本号合法的条目。
21
+ */
22
+ export declare function fetchDistTags(registryUrl: unknown, fetchImpl?: FetchLike): Promise<DistTags>;
23
+ /**
24
+ * 选择要跟踪的 dist-tag。
25
+ * 优先使用显式 channel;auto 时按当前版本的 prerelease 渠道选 tag,
26
+ * 渠道不存在则 latest,仍不存在则取第一个合法 tag。
27
+ * @param distTags dist-tags 表。
28
+ * @param currentVersion 当前安装的版本。
29
+ * @param channelOverride 显式渠道,默认为 auto。
30
+ * @returns 没有合法 tag 时返回 null。
31
+ */
32
+ export declare function pickTarget(distTags: DistTags | null | undefined, currentVersion: string, channelOverride?: string): {
33
+ tag: string;
34
+ version: string;
35
+ } | null;
36
+ /** registry 里的一个已发布版本。 */
37
+ export interface PublishedVersion {
38
+ version: string;
39
+ publishedAt: string | null;
40
+ }
41
+ /**
42
+ * 拉取 `@deepseek-ai/dsh` 的包元数据,取出全部已发布版本与发布时间。
43
+ * @param registryUrl registry 地址,末尾斜杠可有可无。
44
+ * @param fetchImpl fetch 实现,测试时注入。
45
+ * @returns 只保留版本号合法的条目,顺序与 registry 返回一致。
46
+ */
47
+ export declare function fetchPublishedVersions(registryUrl: unknown, fetchImpl?: FetchLike): Promise<PublishedVersion[]>;
48
+ /**
49
+ * 从已发布版本中筛出比当前安装版本更旧的版本,按版本号从新到旧排序。
50
+ * @param versions 已发布版本列表。
51
+ * @param installedVersion 当前安装的版本。
52
+ */
53
+ export declare function selectRollbackCandidates(versions: PublishedVersion[] | null | undefined, installedVersion: string): PublishedVersion[];
@@ -0,0 +1,124 @@
1
+ import { compareVersions, isValidVersion, parseVersion, prereleaseChannel } from "./versions.js";
2
+ // src/registry.ts
3
+ /**
4
+ * npm registry 的 dist-tags 查询、已发布版本列表与回滚候选筛选。
5
+ */
6
+ const DIST_TAGS_PATH = "/-/package/@deepseek-ai%2Fdsh/dist-tags";
7
+ const PACKUMENT_PATH = "/@deepseek-ai%2Fdsh";
8
+ const FETCH_TIMEOUT_MS = 15e3;
9
+ /** 错误消息里去掉 URL 内嵌的凭据,避免把凭据带进界面状态。 */
10
+ function displayUrl(url) {
11
+ const parsed = new URL(url);
12
+ parsed.username = "";
13
+ parsed.password = "";
14
+ return parsed.href;
15
+ }
16
+ /**
17
+ * 拉取 `@deepseek-ai/dsh` 的 dist-tags。
18
+ * @param registryUrl registry 地址,末尾斜杠可有可无。
19
+ * @param fetchImpl fetch 实现,测试时注入。
20
+ * @returns 只保留版本号合法的条目。
21
+ */
22
+ async function fetchDistTags(registryUrl, fetchImpl = fetch) {
23
+ if (typeof registryUrl !== "string" || registryUrl.length === 0) throw new TypeError("registryUrl 必须是非空字符串");
24
+ const url = `${registryUrl.replace(/\/+$/, "")}${DIST_TAGS_PATH}`;
25
+ const response = await fetchImpl(url, {
26
+ headers: { accept: "application/json" },
27
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
28
+ });
29
+ if (response.status !== 200) throw new Error(`registry 返回 HTTP ${response.status}: ${displayUrl(url)}`);
30
+ const payload = await response.json();
31
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new Error(`registry 的 dist-tags 响应不是对象: ${displayUrl(url)}`);
32
+ const distTags = {};
33
+ for (const [tag, version] of Object.entries(payload)) if (typeof version === "string" && isValidVersion(version)) distTags[tag] = version;
34
+ return distTags;
35
+ }
36
+ /**
37
+ * 选择要跟踪的 dist-tag。
38
+ * 优先使用显式 channel;auto 时按当前版本的 prerelease 渠道选 tag,
39
+ * 渠道不存在则 latest,仍不存在则取第一个合法 tag。
40
+ * @param distTags dist-tags 表。
41
+ * @param currentVersion 当前安装的版本。
42
+ * @param channelOverride 显式渠道,默认为 auto。
43
+ * @returns 没有合法 tag 时返回 null。
44
+ */
45
+ function pickTarget(distTags, currentVersion, channelOverride = "auto") {
46
+ const entries = Object.entries(distTags === null || distTags === void 0 ? {} : distTags).filter(([, version]) => typeof version === "string" && isValidVersion(version));
47
+ if (entries.length === 0) return null;
48
+ const lookup = new Map(entries);
49
+ const override = typeof channelOverride === "string" ? channelOverride : "auto";
50
+ if (override !== "auto" && lookup.has(override)) return {
51
+ tag: override,
52
+ version: lookup.get(override)
53
+ };
54
+ const channel = prereleaseChannel(currentVersion);
55
+ if (channel !== null && lookup.has(channel)) return {
56
+ tag: channel,
57
+ version: lookup.get(channel)
58
+ };
59
+ if (lookup.has("latest")) return {
60
+ tag: "latest",
61
+ version: lookup.get("latest")
62
+ };
63
+ const [tag, version] = entries[0];
64
+ return {
65
+ tag,
66
+ version
67
+ };
68
+ }
69
+ /**
70
+ * 拉取 `@deepseek-ai/dsh` 的包元数据,取出全部已发布版本与发布时间。
71
+ * @param registryUrl registry 地址,末尾斜杠可有可无。
72
+ * @param fetchImpl fetch 实现,测试时注入。
73
+ * @returns 只保留版本号合法的条目,顺序与 registry 返回一致。
74
+ */
75
+ async function fetchPublishedVersions(registryUrl, fetchImpl = fetch) {
76
+ if (typeof registryUrl !== "string" || registryUrl.length === 0) throw new TypeError("registryUrl 必须是非空字符串");
77
+ const url = `${registryUrl.replace(/\/+$/, "")}${PACKUMENT_PATH}`;
78
+ const response = await fetchImpl(url, {
79
+ headers: { accept: "application/json" },
80
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
81
+ });
82
+ if (response.status !== 200) throw new Error(`registry 返回 HTTP ${response.status}: ${displayUrl(url)}`);
83
+ const payload = await response.json();
84
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new Error(`registry 的版本列表响应不是对象: ${displayUrl(url)}`);
85
+ const record = payload;
86
+ const versions = record.versions;
87
+ if (versions === null || typeof versions !== "object" || Array.isArray(versions)) throw new Error(`registry 的版本列表响应缺少 versions 字段: ${displayUrl(url)}`);
88
+ const time = record.time;
89
+ const times = time !== null && typeof time === "object" && !Array.isArray(time) ? time : {};
90
+ const published = [];
91
+ for (const version of Object.keys(versions)) {
92
+ if (!isValidVersion(version)) continue;
93
+ const publishedAt = times[version];
94
+ published.push({
95
+ version,
96
+ publishedAt: typeof publishedAt === "string" ? publishedAt : null
97
+ });
98
+ }
99
+ return published;
100
+ }
101
+ /**
102
+ * 从已发布版本中筛出比当前安装版本更旧的版本,按版本号从新到旧排序。
103
+ * @param versions 已发布版本列表。
104
+ * @param installedVersion 当前安装的版本。
105
+ */
106
+ function selectRollbackCandidates(versions, installedVersion) {
107
+ if (parseVersion(installedVersion) === null) throw new TypeError(`installedVersion 不是合法的版本号: ${String(installedVersion)}`);
108
+ const seen = /* @__PURE__ */ new Set();
109
+ const candidates = [];
110
+ for (const entry of Array.isArray(versions) ? versions : []) {
111
+ if (entry === null || typeof entry !== "object") continue;
112
+ const version = entry.version;
113
+ if (typeof version !== "string" || !isValidVersion(version) || seen.has(version)) continue;
114
+ if (compareVersions(version, installedVersion) >= 0) continue;
115
+ seen.add(version);
116
+ candidates.push({
117
+ version,
118
+ publishedAt: typeof entry.publishedAt === "string" ? entry.publishedAt : null
119
+ });
120
+ }
121
+ candidates.sort((left, right) => compareVersions(right.version, left.version));
122
+ return candidates;
123
+ }
124
+ export { fetchDistTags, fetchPublishedVersions, pickTarget, selectRollbackCandidates };
@@ -0,0 +1,80 @@
1
+ /* dsh-version-check 侧边栏徽标、版本面板与设置页的样式。只使用主题 token 与布局常量。 */
2
+ .dvc-layer{position:relative;display:flex;align-items:center;flex:none;width:100%;height:42px;margin:8px 0 0}
3
+ .dvc-badge{display:inline-flex;align-items:center;gap:8px;width:calc(100% + 4px);height:42px;margin:0 -2px;padding:0 10px 0 8px;border:none;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;cursor:pointer;overflow:hidden;text-align:start}
4
+ .dvc-badge:hover,.dvc-badge[data-active]{background:var(--dsw-alias-interactive-bg-hover)}
5
+ .dvc-icon{flex:none;display:block}
6
+ .dvc-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
7
+ .dvc-value{flex:none;margin-left:auto;display:inline-flex;align-items:center;gap:6px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px;font-variant-numeric:tabular-nums}
8
+ .dvc-value[data-tone="update"]{color:var(--dsw-alias-state-business-primary)}
9
+ .dvc-value[data-tone="error"]{color:var(--dsw-alias-state-error-primary)}
10
+ .dvc-dot{flex:none;display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-business-primary)}
11
+ .dvc-layer.dvc-rail{width:36px;height:36px;margin:0}
12
+ .dvc-rail .dvc-badge{position:relative;justify-content:center;gap:0;width:36px;height:36px;margin:0;padding:0;border-radius:50%}
13
+ .dvc-rail-dot{position:absolute;top:3px;right:3px;display:block;width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-business-primary)}
14
+ .dvc-panel{position:fixed;z-index:30;isolation:isolate;display:flex;flex-direction:column;width:360px;max-width:calc(100vw - 24px);max-height:60vh;border:0;border-radius:12px;box-shadow:var(--dsw-elevation-prominent);overflow:hidden}
15
+ .dvc-panel::before{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);pointer-events:none}
16
+ .dvc-header{box-sizing:border-box;flex:none;display:flex;align-items:center;justify-content:space-between;min-height:44px;padding:10px 12px}
17
+ .dvc-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px}
18
+ .dvc-body{flex:1;min-height:0;padding:0 12px 12px;overflow-y:auto;font-size:13px;color:var(--dsw-alias-label-primary)}
19
+ .dvc-version-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 0 10px}
20
+ .dvc-version-block{display:flex;flex-direction:column;gap:2px;min-width:0}
21
+ .dvc-version-label{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
22
+ .dvc-version{color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code,monospace);font-size:18px;font-weight:510;line-height:24px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
23
+ .dvc-pill{flex:none;border-radius:999px;padding:2px 8px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}
24
+ .dvc-pill[data-tone="update"]{color:var(--dsw-alias-state-business-primary)}
25
+ .dvc-pill[data-tone="error"]{color:var(--dsw-alias-state-error-primary)}
26
+ .dvc-row{display:flex;justify-content:space-between;gap:16px;padding:6px 0}
27
+ .dvc-row-label{color:var(--dsw-alias-label-tertiary)}
28
+ .dvc-row-value{min-width:0;color:var(--dsw-alias-label-primary);text-align:end;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
29
+ .dvc-divider{border-top:.5px solid var(--dsw-alias-border-l2)}
30
+ .dvc-error{margin:6px 0 0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
31
+ .dvc-progress{margin:6px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
32
+ .dvc-output{margin:2px 0 0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;word-break:break-all;white-space:pre-wrap}
33
+ .dvc-actions{display:flex;justify-content:flex-end;gap:10px;padding-top:8px}
34
+ .dvc-button{height:36px;padding:0 14px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;cursor:pointer}
35
+ .dvc-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
36
+ .dvc-button[data-primary]{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font-weight:500}
37
+ .dvc-button[data-primary]:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
38
+ .dvc-button:disabled{opacity:.5;cursor:default}
39
+ .dvc-note{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
40
+ .dvc-settings{display:flex;flex-direction:column;padding-bottom:8px;color:var(--dsw-alias-label-primary);font-size:14px;line-height:20px}
41
+ .dvc-settings-blurb{margin:0;padding:4px 0 12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
42
+ .dvc-settings-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
43
+ .dvc-settings-rowText{display:flex;flex-direction:column;gap:4px;min-width:0}
44
+ .dvc-settings-rowTitle{font-size:14px;line-height:20px}
45
+ .dvc-settings-rowDesc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
46
+ .dvc-settings-control{display:flex;flex:none;align-items:center;gap:8px}
47
+ .dvc-switch{box-sizing:border-box;position:relative;flex:none;width:36px;height:20px;padding:2px;border:0;border-radius:999px;corner-shape:round;background:var(--dsw-alias-border-l3);cursor:pointer;appearance:none;-webkit-appearance:none}
48
+ .dvc-switch[aria-checked="true"]{background:var(--dsw-alias-brand-primary)}
49
+ .dvc-switch:disabled{cursor:default;opacity:.5}
50
+ .dvc-switch:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
51
+ .dvc-switchThumb{position:absolute;top:50%;left:2px;display:block;width:16px;height:16px;margin:0;border-radius:50%;corner-shape:round;background:var(--dsw-alias-label-primary-foreground);transform:translateY(-50%);transition:transform .12s ease}
52
+ .dvc-switch[aria-checked="false"] .dvc-switchThumb{background:var(--dsw-alias-switch-thumb)}
53
+ .dvc-switch[aria-checked="true"] .dvc-switchThumb{transform:translate(16px,-50%)}
54
+ .dvc-settings-input{box-sizing:border-box;width:96px;height:32px;padding:0 8px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
55
+ .dvc-settings-input:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
56
+ .dvc-settings-input[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
57
+ .dvc-settings-select{box-sizing:border-box;height:32px;padding:0 8px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
58
+ .dvc-settings-select:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
59
+ .dvc-settings-unit{color:var(--dsw-alias-label-tertiary);font-size:12px}
60
+ .dvc-settings-feedback{min-height:18px;margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
61
+ .dvc-settings-feedback[data-tone="error"]{color:var(--dsw-alias-state-error-primary)}
62
+ .dvc-settings-section{display:flex;flex-direction:column;gap:8px;padding-top:16px}
63
+ .dvc-settings-section .dvc-settings-rowDesc{margin:0}
64
+ .dvc-rollback-header{display:flex;align-items:center;justify-content:space-between;gap:16px}
65
+ .dvc-rollback-picker{display:flex}
66
+ .dvc-rollback-pick{height:32px;padding:0 12px;font-size:13px}
67
+ .dvc-rollback-list{display:flex;flex-direction:column;gap:4px}
68
+ .dvc-rollback-row{display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
69
+ .dvc-rollback-version{flex:none;color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code,monospace);font-size:13px}
70
+ .dvc-rollback-date{flex:1;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
71
+ .dvc-rollback-row .dvc-button{height:28px;padding:0 10px;font-size:12px}
72
+ .dvc-rollback-toggle{align-self:flex-start;height:28px;padding:0 10px;font-size:12px}
73
+ .dvc-rollback-confirmText{margin:0 0 4px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
74
+ .dvc-rollback-confirmActions{display:flex;justify-content:flex-end;gap:8px;padding-top:4px}
75
+ .dvc-rollback-popover{position:fixed;z-index:30;isolation:isolate;display:flex;flex-direction:column;width:320px;max-width:calc(100vw - 24px);max-height:360px;border-radius:12px;box-shadow:var(--dsw-elevation-prominent);overflow:hidden}
76
+ .dvc-rollback-popover::before{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);pointer-events:none}
77
+ .dvc-rollback-popoverHeader{box-sizing:border-box;flex:none;display:flex;align-items:center;min-height:44px;padding:10px 12px}
78
+ .dvc-rollback-popoverTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px}
79
+ .dvc-rollback-popoverBody{flex:1;min-height:0;padding:0 12px 8px;overflow-y:auto}
80
+ .dvc-rollback-popoverFooter{flex:none;padding:8px 12px;border-top:.5px solid var(--dsw-alias-border-l2)}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 版本号解析与比较,按 semver 的优先级规则处理 prerelease。
3
+ * build metadata(`+` 之后的部分)解析时忽略。
4
+ */
5
+ export interface ParsedVersion {
6
+ major: number;
7
+ minor: number;
8
+ patch: number;
9
+ prerelease: string[];
10
+ }
11
+ /**
12
+ * 解析一个版本字符串。
13
+ * @param text 版本字符串,允许 v 前缀与 build metadata。
14
+ * @returns 非法输入返回 null。
15
+ */
16
+ export declare function parseVersion(text: unknown): ParsedVersion | null;
17
+ /**
18
+ * 按 semver 优先级比较两个版本号。
19
+ * 有 prerelease 的版本小于同号正式版;prerelease 标识符逐个比较,
20
+ * 前缀相同时更长的标识符列表更大。
21
+ */
22
+ export declare function compareVersions(a: unknown, b: unknown): -1 | 0 | 1;
23
+ /**
24
+ * 取版本号 prerelease 渠道,即第一个 prerelease 标识符。
25
+ * @param version 版本字符串。
26
+ * @returns 无 prerelease 或版本非法时返回 null。
27
+ */
28
+ export declare function prereleaseChannel(version: unknown): string | null;
29
+ /**
30
+ * 判断字符串是否是一个合法版本号。
31
+ */
32
+ export declare function isValidVersion(text: unknown): boolean;