dsh-plugin-update 0.1.2 → 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.
- package/CHANGELOG.md +49 -0
- package/README.md +483 -255
- package/dist/batch-run.d.ts +80 -0
- package/dist/batch-run.js +86 -0
- package/dist/batch.d.ts +111 -0
- package/dist/batch.js +183 -0
- package/dist/changelog-io.d.ts +56 -0
- package/dist/changelog-io.js +258 -0
- package/dist/changelog.d.ts +96 -0
- package/dist/changelog.js +287 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +75 -1
- package/dist/commands.d.ts +59 -0
- package/dist/commands.js +60 -4
- package/dist/config.d.ts +48 -0
- package/dist/config.js +9 -1
- package/dist/diag.d.ts +125 -0
- package/dist/diag.js +243 -0
- package/dist/entry.d.ts +60 -0
- package/dist/entry.js +305 -0
- package/dist/gate.d.ts +35 -0
- package/dist/host-batch.d.ts +113 -0
- package/dist/host-batch.js +778 -0
- package/dist/host.d.ts +129 -0
- package/dist/host.js +506 -52
- package/dist/panel-batch.d.ts +151 -0
- package/dist/panel-batch.js +927 -0
- package/dist/panel.d.ts +256 -0
- package/dist/panel.js +1300 -0
- package/dist/ports.d.ts +208 -0
- package/dist/queue.d.ts +109 -0
- package/dist/queue.js +182 -0
- package/dist/reader.d.ts +84 -0
- package/dist/reader.js +65 -6
- package/dist/redaction.d.ts +120 -0
- package/dist/redaction.js +205 -0
- package/dist/service.d.ts +72 -0
- package/dist/service.js +100 -6
- package/dist/store.d.ts +229 -0
- package/dist/store.js +251 -12
- package/package.json +27 -2
package/dist/service.js
CHANGED
|
@@ -7,7 +7,12 @@ const RECHECK_WINDOW_MS = 2e3;
|
|
|
7
7
|
const MAX_METADATA_BYTES = 256 * 1024;
|
|
8
8
|
const INTEGRITY_PATTERN = "^sha512-[A-Za-z0-9+/]{86}==$";
|
|
9
9
|
function validRequestId(v) {
|
|
10
|
-
|
|
10
|
+
if (typeof v !== "string") return false;
|
|
11
|
+
const id = v.trim();
|
|
12
|
+
if (id.length < 1 || id.length > 128) return false;
|
|
13
|
+
if (!/^[A-Za-z0-9._~-]+$/.test(id)) return false;
|
|
14
|
+
if (/^(npm_|gh[pousr]_|github_pat_|sk-|bearer)/i.test(id)) return false;
|
|
15
|
+
return true;
|
|
11
16
|
}
|
|
12
17
|
function updateError(code) {
|
|
13
18
|
return Object.assign(new Error(code), { code });
|
|
@@ -15,6 +20,37 @@ function updateError(code) {
|
|
|
15
20
|
function validVersion(v) {
|
|
16
21
|
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
|
|
17
22
|
}
|
|
23
|
+
const RELEASE_CHANNELS = ["stable", "prerelease"];
|
|
24
|
+
function validReleaseChannel(v) {
|
|
25
|
+
return v === "stable" || v === "prerelease";
|
|
26
|
+
}
|
|
27
|
+
function validPrereleaseIds(ids) {
|
|
28
|
+
if (typeof ids !== "string" || !ids) return false;
|
|
29
|
+
const parts = ids.split(".");
|
|
30
|
+
if (parts.length === 0) return false;
|
|
31
|
+
for (const p of parts) {
|
|
32
|
+
if (!p || !/^[0-9A-Za-z-]+$/.test(p)) return false;
|
|
33
|
+
if (/^\d+$/.test(p) && p.length > 1 && p.startsWith("0")) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function validReleaseVersion(v) {
|
|
38
|
+
if (typeof v !== "string" || !v) return false;
|
|
39
|
+
const dash = v.indexOf("-");
|
|
40
|
+
if (dash < 0) return validVersion(v);
|
|
41
|
+
const core = v.slice(0, dash);
|
|
42
|
+
const ids = v.slice(dash + 1);
|
|
43
|
+
if (!validVersion(core) || !validPrereleaseIds(ids)) return false;
|
|
44
|
+
if (ids.includes("+")) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function isPrereleaseVersion(v) {
|
|
48
|
+
return validReleaseVersion(v) && !validVersion(v);
|
|
49
|
+
}
|
|
50
|
+
function isVersionAllowedInChannel(version, channel = "stable") {
|
|
51
|
+
if (channel === "prerelease") return validReleaseVersion(version);
|
|
52
|
+
return validVersion(version);
|
|
53
|
+
}
|
|
18
54
|
function parseTriple(v) {
|
|
19
55
|
const parts = String(v).split(".");
|
|
20
56
|
if (parts.length > 3) return null;
|
|
@@ -38,6 +74,40 @@ function compareVersions(a, b) {
|
|
|
38
74
|
}
|
|
39
75
|
return 0;
|
|
40
76
|
}
|
|
77
|
+
function parseReleaseIds(ids) {
|
|
78
|
+
return ids.split(".").map((p) => /^\d+$/.test(p) ? Number(p) : p);
|
|
79
|
+
}
|
|
80
|
+
function compareReleaseVersions(a, b) {
|
|
81
|
+
if (!validReleaseVersion(a) || !validReleaseVersion(b)) throw updateError("invalid-release");
|
|
82
|
+
const dashA = String(a).indexOf("-");
|
|
83
|
+
const dashB = String(b).indexOf("-");
|
|
84
|
+
const coreA = dashA < 0 ? String(a) : String(a).slice(0, dashA);
|
|
85
|
+
const coreB = dashB < 0 ? String(b) : String(b).slice(0, dashB);
|
|
86
|
+
const order = compareVersions(coreA, coreB);
|
|
87
|
+
if (order !== 0) return order;
|
|
88
|
+
const preA = dashA < 0 ? null : parseReleaseIds(String(a).slice(dashA + 1));
|
|
89
|
+
const preB = dashB < 0 ? null : parseReleaseIds(String(b).slice(dashB + 1));
|
|
90
|
+
if (preA === null && preB === null) return 0;
|
|
91
|
+
if (preA === null) return 1;
|
|
92
|
+
if (preB === null) return -1;
|
|
93
|
+
const width = Math.max(preA.length, preB.length);
|
|
94
|
+
for (let i = 0; i < width; i++) {
|
|
95
|
+
const x = preA[i];
|
|
96
|
+
const y = preB[i];
|
|
97
|
+
if (x === void 0) return -1;
|
|
98
|
+
if (y === void 0) return 1;
|
|
99
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
100
|
+
if (x < y) return -1;
|
|
101
|
+
if (x > y) return 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (typeof x === "number") return -1;
|
|
105
|
+
if (typeof y === "number") return 1;
|
|
106
|
+
if (x < y) return -1;
|
|
107
|
+
if (x > y) return 1;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
41
111
|
function compareOne(node, op, target) {
|
|
42
112
|
const order = compareTriple(node, target);
|
|
43
113
|
if (op === "=" || op === "") return order === 0;
|
|
@@ -102,9 +172,20 @@ function timeoutSignal(ms) {
|
|
|
102
172
|
}
|
|
103
173
|
return void 0;
|
|
104
174
|
}
|
|
175
|
+
function httpStatusOf(response) {
|
|
176
|
+
const raw = response?.status;
|
|
177
|
+
return typeof raw === "number" && Number.isInteger(raw) && raw >= 100 && raw <= 599 ? raw : null;
|
|
178
|
+
}
|
|
179
|
+
function checkFailedWithStatus(response) {
|
|
180
|
+
const err = updateError("check-failed");
|
|
181
|
+
const status = httpStatusOf(response);
|
|
182
|
+
if (status !== null) err.httpStatus = status;
|
|
183
|
+
return err;
|
|
184
|
+
}
|
|
105
185
|
async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
106
186
|
const targetName = opts?.targetPackageName ?? PACKAGE_NAME;
|
|
107
187
|
const registry = opts?.registryUrl ?? NPM_REGISTRY;
|
|
188
|
+
const channel = opts?.releaseChannel === "prerelease" ? "prerelease" : "stable";
|
|
108
189
|
let response = null;
|
|
109
190
|
try {
|
|
110
191
|
response = await fetchImpl(`${registry}${encodeURIComponent(targetName)}/latest`, {
|
|
@@ -116,13 +197,13 @@ async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
|
116
197
|
throw updateError("check-failed");
|
|
117
198
|
}
|
|
118
199
|
try {
|
|
119
|
-
if (!response.ok) throw
|
|
200
|
+
if (!response.ok) throw checkFailedWithStatus(response);
|
|
120
201
|
const declared = Number(response.headers.get("content-length"));
|
|
121
202
|
if (Number.isFinite(declared) && declared > MAX_METADATA_BYTES) throw updateError("invalid-release");
|
|
122
203
|
const text = await response.text();
|
|
123
204
|
if (byteLength(text) > MAX_METADATA_BYTES) throw updateError("invalid-release");
|
|
124
205
|
const value = JSON.parse(text);
|
|
125
|
-
if (value.name !== targetName || !
|
|
206
|
+
if (value.name !== targetName || !isVersionAllowedInChannel(value.version, channel)) throw updateError("invalid-release");
|
|
126
207
|
const version = value.version;
|
|
127
208
|
const nodeRange = value.engines?.node;
|
|
128
209
|
if (nodeRange !== void 0 && typeof nodeRange !== "string") throw updateError("invalid-release");
|
|
@@ -142,7 +223,7 @@ async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
|
142
223
|
if (error?.code === "check-failed") throw error;
|
|
143
224
|
if (error?.code === "invalid-release") throw error;
|
|
144
225
|
if (response !== null && response.ok) throw updateError("invalid-release");
|
|
145
|
-
throw
|
|
226
|
+
throw checkFailedWithStatus(response);
|
|
146
227
|
}
|
|
147
228
|
}
|
|
148
229
|
function createUpdateCore(ports) {
|
|
@@ -150,8 +231,9 @@ function createUpdateCore(ports) {
|
|
|
150
231
|
const confirmationTtlMs = ports.confirmationTtlMs ?? CONFIRMATION_TTL_MS;
|
|
151
232
|
const targetPackageName = ports.targetPackageName ?? PACKAGE_NAME;
|
|
152
233
|
const registryUrl = ports.registryUrl ?? NPM_REGISTRY;
|
|
234
|
+
const releaseChannel = ports.releaseChannel === "prerelease" ? "prerelease" : "stable";
|
|
153
235
|
function fetchRelease() {
|
|
154
|
-
return fetchNpmRelease(ports.fetchImpl, checkTimeoutMs, { targetPackageName, registryUrl });
|
|
236
|
+
return fetchNpmRelease(ports.fetchImpl, checkTimeoutMs, { targetPackageName, registryUrl, releaseChannel });
|
|
155
237
|
}
|
|
156
238
|
let checked = null;
|
|
157
239
|
let checking = null;
|
|
@@ -215,8 +297,14 @@ function createUpdateCore(ports) {
|
|
|
215
297
|
const busy = job?.state === "installing" || job?.state === "verifying";
|
|
216
298
|
const fresh = checked !== null && checked.checkId !== null && ports.now() < checked.expiresAt;
|
|
217
299
|
const runningVersion = ports.readRunningVersion();
|
|
300
|
+
let newer = false;
|
|
301
|
+
try {
|
|
302
|
+
newer = isVersionAllowedInChannel(runningVersion, releaseChannel) && !!checked?.release && isVersionAllowedInChannel(checked.release.version, releaseChannel) && compareReleaseVersions(checked.release.version, runningVersion) === 1;
|
|
303
|
+
} catch {
|
|
304
|
+
newer = false;
|
|
305
|
+
}
|
|
218
306
|
const canInstall = Boolean(
|
|
219
|
-
env.eligible && !blockedReason && !busy && fresh && checked?.installationKey === env.installationKey &&
|
|
307
|
+
env.eligible && !blockedReason && !busy && fresh && checked?.installationKey === env.installationKey && newer
|
|
220
308
|
);
|
|
221
309
|
return {
|
|
222
310
|
runningVersion,
|
|
@@ -353,11 +441,17 @@ export {
|
|
|
353
441
|
NPM_REGISTRY,
|
|
354
442
|
PACKAGE_NAME,
|
|
355
443
|
RECHECK_WINDOW_MS,
|
|
444
|
+
RELEASE_CHANNELS,
|
|
445
|
+
compareReleaseVersions,
|
|
356
446
|
compareVersions,
|
|
357
447
|
createUpdateCore,
|
|
358
448
|
fetchNpmRelease,
|
|
449
|
+
isPrereleaseVersion,
|
|
450
|
+
isVersionAllowedInChannel,
|
|
359
451
|
satisfiesNodeRange,
|
|
360
452
|
updateError,
|
|
453
|
+
validReleaseChannel,
|
|
454
|
+
validReleaseVersion,
|
|
361
455
|
validRequestId,
|
|
362
456
|
validVersion
|
|
363
457
|
};
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/store.ts —— 更新任务的落盘与执行跑腿。
|
|
3
|
+
*
|
|
4
|
+
* 由 src/host/updateStore.js 改写拎入:只跑腿不决策。相对旧实现的增量(规格 #591):
|
|
5
|
+
* 1. 状态目录按插件标识派生(第 3 条):join(家目录, 'updates', 插件标识, 使用范围短指纹),
|
|
6
|
+
* 三文件名保持 state.json、install.lock、before.json 不变。
|
|
7
|
+
* 2. 默认旧路径冻结(第 14 条):插件标识取旧值 dsh-mattpocock-skills-deck 时路径与旧原文
|
|
8
|
+
* 一字不差——下面的旧字面是永久冻结特例,注释永不删除(动默认即破冰)。
|
|
9
|
+
* 3. 读走双读、写只写新(第 7 条):先读新路径,没有再回退读旧路径;写只写新路径,
|
|
10
|
+
* 旧路径只读不写永久留作只读影子。旧文件不动,清理另票再议。
|
|
11
|
+
* 4. 三处注入默认现状(第 4 条):目标包名、官方源、安装时限经零件注入,默认值等于现状。
|
|
12
|
+
* 5. 安装执行日志带必填插件标识(第 8、13 条):update.install.exec 加完标识后 5 键为基线。
|
|
13
|
+
*
|
|
14
|
+
* 安装执行按核心给的配方跑三条路由之一(第三方 Desktop 走桌面服务、官方桌面版交给宿主
|
|
15
|
+
* 进程内的插件管理器、普通 DSH 自己起进程),
|
|
16
|
+
* 本文件不按操作系统分支,也不拼 shell。决策顺序全在更新核心。测试一律用假零件,
|
|
17
|
+
* 不真写盘真跑命令(经可选的文件读写函数注入,默认走真文件系统)。
|
|
18
|
+
*/
|
|
19
|
+
import { type BatchSession } from './batch.js';
|
|
20
|
+
import { type UpdateQueueState } from './queue.js';
|
|
21
|
+
import type { EnvironmentKind, UpdateJob } from './ports.js';
|
|
22
|
+
export interface UpdatePaths {
|
|
23
|
+
directory: string;
|
|
24
|
+
state: string;
|
|
25
|
+
lock: string;
|
|
26
|
+
backup: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 状态目录:家目录下按插件标识派生,短指纹只做同一性判断,不记原文。
|
|
30
|
+
* 插件标识取旧值时与旧原文一字不差(旧默认路径冻结,验收项)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function pathsForUpdate(homeDir: string, pluginId: string, profileDir: string): UpdatePaths | null;
|
|
33
|
+
/** 旧只读影子目录(规格 #591 第 7 条):只读不写,永久留作回退读取用。 */
|
|
34
|
+
export declare function legacyPathsForUpdate(homeDir: string, profileDir: string): UpdatePaths | null;
|
|
35
|
+
export interface DiskPortsDeps {
|
|
36
|
+
readFileImpl?: (filename: string, encoding: string) => Promise<string>;
|
|
37
|
+
statImpl?: (filename: string) => Promise<{
|
|
38
|
+
size: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 建任务存取与锁:调用方按当前插件标识建一份,转交核心当零件。
|
|
43
|
+
* 读走双读(先新后旧),写只写新(旧影子只读不写)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function createUpdateDiskPorts(homeDir: string, pluginId: string, profileDir: string, deps?: DiskPortsDeps): {
|
|
46
|
+
paths: UpdatePaths | null;
|
|
47
|
+
legacyPaths: UpdatePaths | null;
|
|
48
|
+
readJob: () => Promise<UpdateJob | null>;
|
|
49
|
+
writeJob: (job: UpdateJob | null) => Promise<void>;
|
|
50
|
+
tryAcquireLock: (lockId: string) => Promise<boolean>;
|
|
51
|
+
releaseLock: (lockId: string) => Promise<void>;
|
|
52
|
+
backupJob: (job: UpdateJob) => Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
/** 跨插件队列的共享落盘(#15,新增强制串行的归属与持久化,不碰按插件隔离的旧树)。
|
|
55
|
+
*
|
|
56
|
+
* 归属:按使用范围共享——同一 (homeDir, profileDir) 的全部插件共用同一目录,
|
|
57
|
+
* 不同使用范围各用各的、可并行。父目录取 `update-queue`(与 `updates` 平级),
|
|
58
|
+
* 任何插件标识都撞不上(插件标识只能决定 `updates` 下的子目录名)。
|
|
59
|
+
* 文件:`queue.json`(owner + waiting,快照式整写),`global.lock`(执行互斥,
|
|
60
|
+
* 抢到才许装,一次只装一个)。
|
|
61
|
+
*/
|
|
62
|
+
export declare const QUEUE_DIR_SEGMENT = "update-queue";
|
|
63
|
+
export declare const QUEUE_FILE = "queue.json";
|
|
64
|
+
export declare const QUEUE_LOCK_FILE = "global.lock";
|
|
65
|
+
/** 全局锁陈旧回收默认口径:15 分钟(与安装时限同口径,调用方可按注入覆盖)。 */
|
|
66
|
+
export declare const QUEUE_LOCK_STALE_MS: number;
|
|
67
|
+
export interface UpdateQueuePaths {
|
|
68
|
+
directory: string;
|
|
69
|
+
file: string;
|
|
70
|
+
lock: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 队列共享目录:家目录下按使用范围短指纹派生(短指纹算法与按插件隔离的旧树同一套,
|
|
74
|
+
* 只做同一性判断,不记原文)。入参非法返回 null,调用方按“无队列可用”放行安装,
|
|
75
|
+
* 绝不因队列自身故障挡住更新。
|
|
76
|
+
*/
|
|
77
|
+
export declare function queuePathsForUpdate(homeDir: string, profileDir: string): UpdateQueuePaths | null;
|
|
78
|
+
export interface QueuePortsDeps {
|
|
79
|
+
readFileImpl?: (filename: string, encoding: string) => Promise<string>;
|
|
80
|
+
statImpl?: (filename: string) => Promise<{
|
|
81
|
+
size: number;
|
|
82
|
+
}>;
|
|
83
|
+
nowImpl?: () => number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 建跨插件队列存取与全局锁:调用方按当前使用范围建一份(与插件标识无关),转交宿主入口组合进安装锁。
|
|
87
|
+
* 读走单读(队列文件只此一份,没有旧影子);写原子整写;锁走独占建文件,过期可回收一次。
|
|
88
|
+
*/
|
|
89
|
+
export declare function createUpdateQueuePorts(homeDir: string, profileDir: string, deps?: QueuePortsDeps): {
|
|
90
|
+
paths: UpdateQueuePaths | null;
|
|
91
|
+
readQueue: () => Promise<UpdateQueueState>;
|
|
92
|
+
writeQueue: (state: UpdateQueueState) => Promise<void>;
|
|
93
|
+
tryAcquireGlobalLock: (lockId: string, pluginId: string, opts?: {
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
}) => Promise<boolean>;
|
|
96
|
+
releaseGlobalLock: (lockId: string) => Promise<void>;
|
|
97
|
+
};
|
|
98
|
+
/** 批量会话账本的共享落盘(#25,与队列同一目录:同一使用范围一份,跨插件共用)。
|
|
99
|
+
*
|
|
100
|
+
* 归属:按使用范围共享(与跨插件队列同一套目录派生:`update-queue/<使用范围短指纹>`),
|
|
101
|
+
* 不同使用范围各用各的。文件只有一份 `batch.json` —— 「这一轮批量更新走到哪了」是全范围唯一的一份,
|
|
102
|
+
* 与按插件隔离的 `updates/<插件标识>/...` 那棵树完全分开(父目录不同,任何插件标识都撞不上)。
|
|
103
|
+
* 纪律与队列同一套:读坏自愈为空会话(缺文件、超大、JSON 坏掉、读失败都回空,绝不因账本坏掉挡住更新);
|
|
104
|
+
* 写走原子整写(临时文件 + rename,写完删临时件);清空写一份空会话(不是删文件,避免与并发读打架)。
|
|
105
|
+
*/
|
|
106
|
+
export declare const BATCH_FILE = "batch.json";
|
|
107
|
+
/** 账本读入上限:1 MiB,超了当坏账本(正常一份几百字节)。 */
|
|
108
|
+
export declare const BATCH_FILE_MAX_BYTES: number;
|
|
109
|
+
export interface UpdateBatchPaths {
|
|
110
|
+
directory: string;
|
|
111
|
+
file: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 批量账本路径:与队列同一目录(`update-queue/<短指纹>/batch.json`)。
|
|
115
|
+
* 入参非法返回 null,调用方按「账本不可落盘」处理(诚实说明,绝不猜目录)。
|
|
116
|
+
*/
|
|
117
|
+
export declare function batchPathsForUpdate(homeDir: string, profileDir: string): UpdateBatchPaths | null;
|
|
118
|
+
export interface BatchPortsDeps {
|
|
119
|
+
readFileImpl?: (filename: string, encoding: string) => Promise<string>;
|
|
120
|
+
statImpl?: (filename: string) => Promise<{
|
|
121
|
+
size: number;
|
|
122
|
+
}>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 建批量账本存取:调用方按当前使用范围建一份(与插件标识无关),转交批量宿主入口。
|
|
126
|
+
* 读走单读自愈为空会话;写原子整写;清空写空会话。路径算不出来时不抛错(读回空、写抛 batch-unavailable)。
|
|
127
|
+
*/
|
|
128
|
+
export declare function createBatchDiskPorts(homeDir: string, profileDir: string, deps?: BatchPortsDeps): {
|
|
129
|
+
paths: UpdateBatchPaths | null;
|
|
130
|
+
readBatch: () => Promise<BatchSession>;
|
|
131
|
+
writeBatch: (session: BatchSession) => Promise<void>;
|
|
132
|
+
clearBatch: () => Promise<void>;
|
|
133
|
+
};
|
|
134
|
+
/** 按插件 + 版本持久化的跳过记录(#16,与三冻结落盘名并存的第四文件,不碰旧树)。
|
|
135
|
+
*
|
|
136
|
+
* 键:跳过只按「插件标识 + 版本」记——目录已按插件标识隔离(pathsForUpdate),
|
|
137
|
+
* 文件内只记版本集合。新版本恒重新提醒(跳过是 dismissal 不是全局静音)。
|
|
138
|
+
* 重置入口(第一性原理结论,见 #16):已跳过版本留在同一更新横幅行展示为
|
|
139
|
+
* 「已跳过 X.Y.Z · 恢复」,点恢复即清掉该版本的跳过并重查;不藏进设置页。
|
|
140
|
+
* 文件:`skipped.json`(`{ skipped: [{ version, skippedAt }] }`,快照式整写,上限 50 条)。
|
|
141
|
+
*/
|
|
142
|
+
export interface SkippedVersionEntry {
|
|
143
|
+
version: string;
|
|
144
|
+
skippedAt: number;
|
|
145
|
+
}
|
|
146
|
+
/** 跳过文件上限:只留最近 50 条,避免无界增长(老条目自然淘汰)。 */
|
|
147
|
+
export declare const MAX_SKIPPED_ENTRIES = 50;
|
|
148
|
+
/** 跳过展示键:面板行展示与去重用(`插件标识@版本`)。 */
|
|
149
|
+
export declare function skipKey(pluginId: string, version: string): string;
|
|
150
|
+
/** 跳过记录归一化:只收发行合法版(stable + 预发布),非法条目丢弃(自愈不抛错)。 */
|
|
151
|
+
export declare function normalizeSkipped(raw: unknown): SkippedVersionEntry[];
|
|
152
|
+
/** 该版本是否已被跳过(版本须发行合法,否则为 false)。 */
|
|
153
|
+
export declare function isVersionSkipped(skipped: readonly SkippedVersionEntry[] | unknown, version: unknown): boolean;
|
|
154
|
+
/** 记一次跳过:同版本幂等(刷新 skippedAt),新版本追加并按上限裁剪。 */
|
|
155
|
+
export declare function addSkipped(skipped: readonly SkippedVersionEntry[] | unknown, version: string, now: number): SkippedVersionEntry[];
|
|
156
|
+
/** 重置跳过:给了版本只清该版本(面板「恢复」走这条),不给清全部。 */
|
|
157
|
+
export declare function clearSkipped(skipped: readonly SkippedVersionEntry[] | unknown, version?: string): SkippedVersionEntry[];
|
|
158
|
+
export interface SkipPortsDeps {
|
|
159
|
+
readFileImpl?: (filename: string, encoding: string) => Promise<string>;
|
|
160
|
+
statImpl?: (filename: string) => Promise<{
|
|
161
|
+
size: number;
|
|
162
|
+
}>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 建跳过存取:调用方按当前插件标识建一份(目录与任务落盘同一套,无旧影子)。
|
|
166
|
+
* 读坏自愈为空(缺文件、超大、JSON 坏掉都回 [],绝不因跳过文件挡住更新);
|
|
167
|
+
* 写走原子整写。
|
|
168
|
+
*/
|
|
169
|
+
export declare function createSkipDiskPorts(homeDir: string, pluginId: string, profileDir: string, deps?: SkipPortsDeps): {
|
|
170
|
+
file: string | null;
|
|
171
|
+
readSkipped: () => Promise<SkippedVersionEntry[]>;
|
|
172
|
+
writeSkipped: (skipped: readonly SkippedVersionEntry[] | unknown) => Promise<void>;
|
|
173
|
+
clearSkippedVersion: (version?: string) => Promise<SkippedVersionEntry[]>;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* 从正在运行的入口反查 CLI 的 JS 入口:逐级向上找名字为 @deepseek-ai/dsh 的包,
|
|
177
|
+
* 且该文件必须正好是包声明的可执行入口(bin.dsh 或 bin 字符串)。对不上就返回 null,
|
|
178
|
+
* 绝不用 PATH 上的 `dsh` 命令名——否则可能装到别的使用范围或用错版本。
|
|
179
|
+
* 第二参只给测试注入假清单读取器与假路径解析用(默认都走真文件系统)。
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveCliEntry(argv1: string, deps?: {
|
|
182
|
+
readManifest?: (directory: string) => Promise<Record<string, unknown> | null>;
|
|
183
|
+
realpath?: (path: string) => Promise<string>;
|
|
184
|
+
}): Promise<string | null>;
|
|
185
|
+
export interface ExecutorParts {
|
|
186
|
+
profileName?: string | null | (() => string | null);
|
|
187
|
+
environmentKind?: EnvironmentKind | (() => EnvironmentKind);
|
|
188
|
+
profileDir?: string | (() => string);
|
|
189
|
+
subprocess?: unknown | (() => unknown);
|
|
190
|
+
desktopPnpm?: unknown | (() => unknown);
|
|
191
|
+
desktopProfiles?: unknown | (() => unknown);
|
|
192
|
+
/** 官方桌面版进程内的插件管理器(现取,不缓存跨代服务)。 */
|
|
193
|
+
pluginManager?: unknown | (() => unknown);
|
|
194
|
+
runtimeExecutable?: string | (() => string | undefined);
|
|
195
|
+
runtimeExecArgs?: string[] | (() => string[] | undefined);
|
|
196
|
+
cliEntry?: string | (() => string | undefined);
|
|
197
|
+
targetPackageName?: string | (() => string);
|
|
198
|
+
registryUrl?: string | (() => string);
|
|
199
|
+
installTimeoutMs?: number | (() => number);
|
|
200
|
+
pluginId?: string | (() => string);
|
|
201
|
+
log?: (level: string, event: string, fields: Record<string, unknown>) => void;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 真执行器:按核心给的配方跑安装(测试一律注入假零件,不走这里)。
|
|
205
|
+
*
|
|
206
|
+
* 路由只有三条,都由核心的 installRecipe 决定,本文件不按操作系统分支:
|
|
207
|
+
* - desktop-service:桌面宿主公开的 desktopPnpm.runPlugin(参数数组, 使用范围目录);
|
|
208
|
+
* - desktop-manager:官方桌面版进程内的 pluginManager.installBundle(精确版本规格, 选项);
|
|
209
|
+
* - cli-process:subprocess.spawn({ argv: [运行时, …运行时参数, CLI 入口, 'plugin', '--profile', 名, …参数] })。
|
|
210
|
+
* 起进程只经宿主注入的子进程能力,不经 shell、不用 PATH 上的 `dsh` 命令名。
|
|
211
|
+
*/
|
|
212
|
+
export declare function createUpdateExecutor(parts?: ExecutorParts): (args?: {
|
|
213
|
+
version?: string;
|
|
214
|
+
profileName?: string | null;
|
|
215
|
+
environmentKind?: EnvironmentKind;
|
|
216
|
+
}) => Promise<void>;
|
|
217
|
+
export declare const __executorInternals: {
|
|
218
|
+
INSTALL_STDIO: {
|
|
219
|
+
stdin: string;
|
|
220
|
+
stdout: {
|
|
221
|
+
maxBytes: number;
|
|
222
|
+
};
|
|
223
|
+
stderr: {
|
|
224
|
+
maxBytes: number;
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
TERMINATION_GRACE_MS: number;
|
|
228
|
+
CLI_PACKAGE_NAME: string;
|
|
229
|
+
};
|