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.
- package/CHANGELOG.md +49 -0
- package/README.md +483 -279
- 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 +479 -41
- 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 +6 -4
- 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/ports.d.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ports.ts —— 更新模块的插口定义(规格 #591 的字面落实)。
|
|
3
|
+
*
|
|
4
|
+
* 由 update-core/src/ports.ts 原样拎入:六边形架构不变,核心只管做决定,插口由核心定,
|
|
5
|
+
* 跑腿的活全在外面。检验标准仍是核心不碰硬盘、网络、子进程也能被测透,所以本文件只放类型,
|
|
6
|
+
* 不放任何运行时逻辑;转译出的 JS 是空壳,第二家直接吃 TS 源码。
|
|
7
|
+
*
|
|
8
|
+
* 相对原样的增量(规格 #591 要求的新增注入点位,不动冻结的入参回参形状):
|
|
9
|
+
* 插件标识、目标包名、官方源、安装时限经 UpdatePorts 的可选字段注入,默认值等于现状。
|
|
10
|
+
* 三个对外方法、快照六字段、任务公开形状、检查凭证形状、配方五键形状全部冻结不变。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 宿主种类:核心按它选安装路由。
|
|
14
|
+
* - `desktop`:第三方 Desktop 宿主(发布 `desktopProfiles`/`desktopPnpm` 的那一支);
|
|
15
|
+
* - `desktop-manager`:官方桌面版(宿主进程内 `pluginManager.installBundle` 那一支);
|
|
16
|
+
* - `cli`:普通 DSH 宿主(本包自己起命令行)。
|
|
17
|
+
*
|
|
18
|
+
* 取值可增:新增取值属向后兼容,调用方不认就当普通宿主处理(诚实失败转手工命令);
|
|
19
|
+
* 值按时间可调,键与形状仍冻结(见 README 第 5 节)。
|
|
20
|
+
*/
|
|
21
|
+
export type EnvironmentKind = 'desktop' | 'desktop-manager' | 'cli';
|
|
22
|
+
/** 版本通道(#16):默认只推 stable;prerelease 须调用方显式 opt-in。 */
|
|
23
|
+
export type ReleaseChannel = 'stable' | 'prerelease';
|
|
24
|
+
/** 装不了的原因(装前不满足条件只给原因不给装,见通用方案复用清单)。 */
|
|
25
|
+
export type BlockedReason = 'unknown-profile' | 'source-install' | 'invalid-installation' | 'installation-changed' | 'pending-restart' | 'registry-conflict' | 'incompatible-node' | 'recovery-required';
|
|
26
|
+
/** 核心定错误码,适配器只负责把外面世界的脏错误翻译进来。 */
|
|
27
|
+
export type UpdateErrorCode = 'check-failed' | 'invalid-release' | 'check-expired' | 'installation-changed' | 'update-busy' | 'install-failed' | 'unsupported' | 'pending-restart' | 'source-install' | 'unknown-profile' | 'invalid-installation' | 'incompatible-node' | 'registry-conflict' | 'recovery-required';
|
|
28
|
+
/** 快照:恰好六个字段,永远不带钥匙(检查编号与环境指纹另交)。 */
|
|
29
|
+
export interface UpdateSnapshot {
|
|
30
|
+
/** 运行版本:宿主自带清单里的版本号。 */
|
|
31
|
+
runningVersion: string;
|
|
32
|
+
/** 磁盘已装版本:已安装包里的清单版本号(读不到为 null)。 */
|
|
33
|
+
installedVersion: string | null;
|
|
34
|
+
/** 远端最新版本:最近一次查新版带回的版本(没查过为 null)。 */
|
|
35
|
+
latestVersion: string | null;
|
|
36
|
+
/** 能不能装:环境允许、无阻拦原因、有没过期的检查且指纹一致、远端确实更新。 */
|
|
37
|
+
canInstall: boolean;
|
|
38
|
+
/** 装不了的原因(能装为 null)。 */
|
|
39
|
+
blockedReason: BlockedReason | null;
|
|
40
|
+
/** 当前任务:只读半程恒为 null,安装票再接真实任务。 */
|
|
41
|
+
job: UpdateJob | null;
|
|
42
|
+
}
|
|
43
|
+
/** 任务的公开形状(长任务状态随时可问,见通用方案第 5 步)。 */
|
|
44
|
+
export interface UpdateJob {
|
|
45
|
+
id: string;
|
|
46
|
+
state: 'installing' | 'verifying' | 'restart-required' | 'completed' | 'failed' | 'interrupted';
|
|
47
|
+
targetVersion: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* 任务说明:失败时是「失败码」或「失败码: 详情」(详情来自宿主原话,已截断、已去掉绝对路径)。
|
|
50
|
+
* 取值可变,字段与形状不变;要匹配请匹配前缀失败码,不要整串相等(README 第 12 节)。
|
|
51
|
+
*/
|
|
52
|
+
message: string | null;
|
|
53
|
+
/** 提交时的请求编号:同一个编号重复提交直接返回旧结果,不重装。 */
|
|
54
|
+
requestId: string | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 检查凭证:查新版把快照和凭证分开交。
|
|
58
|
+
* 凭证是“用户当时点头的那一次检查”的绑定(编号加当时看到的环境指纹),
|
|
59
|
+
* 装更新时调用方把凭证递回来,快照里永远不带钥匙。
|
|
60
|
+
*/
|
|
61
|
+
export interface CheckReceipt {
|
|
62
|
+
checkId: string;
|
|
63
|
+
checkedAt: number;
|
|
64
|
+
expiresAt: number;
|
|
65
|
+
}
|
|
66
|
+
/** 查新版一次返回:快照与凭证分家。 */
|
|
67
|
+
export interface CheckResult {
|
|
68
|
+
snapshot: UpdateSnapshot;
|
|
69
|
+
receipt: CheckReceipt | null;
|
|
70
|
+
}
|
|
71
|
+
/** 远端发行信息:只接受名字对得上、版本合法、包地址与完整性校验全合规的返回。 */
|
|
72
|
+
export interface ReleaseInfo {
|
|
73
|
+
version: string;
|
|
74
|
+
nodeRange: string;
|
|
75
|
+
integrity: string;
|
|
76
|
+
tarball: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 安装执行路由:三条,由核心按宿主种类选(见 installRecipe),适配器只负责把它跑起来。
|
|
80
|
+
* - `desktop-service`:第三方 Desktop 宿主公开的 `desktopPnpm.runPlugin`;
|
|
81
|
+
* - `desktop-manager`:官方桌面版进程内的 `pluginManager.installBundle`;
|
|
82
|
+
* - `cli-process`:普通 DSH 宿主自己起命令行。
|
|
83
|
+
*/
|
|
84
|
+
export type InstallRoute = 'desktop-service' | 'desktop-manager' | 'cli-process';
|
|
85
|
+
/**
|
|
86
|
+
* 安装执行配方:核心给适配器的完整政策产物(冻结五键:改键改模板即破冰,值按时间可调)。
|
|
87
|
+
* 一律「程序 + 参数数组」——pluginArgs 是**这条路自己的**参数数组,按路由解释:
|
|
88
|
+
* - `desktop-service`/`cli-process`:pnpm 形状(`add --save-exact <包名>@<版本> --registry=<源>`);
|
|
89
|
+
* - `desktop-manager`:宿主管理器形状(`add <包名>@<精确版本>`,一个 spec 字符串、不带任何开关)。
|
|
90
|
+
* 使用范围名不做引号包裹也不按空格拆分(引号只出现在给用户看的手工命令里)。
|
|
91
|
+
* 源按路由落地:前两条进参数数组,`desktop-manager` 走管理器自己的 `registry` 选项(取同一个注入值)。
|
|
92
|
+
*/
|
|
93
|
+
export interface InstallRecipe {
|
|
94
|
+
route: InstallRoute;
|
|
95
|
+
/** 安装目标使用范围名,原样保留(含空格与非 ASCII 字符)。 */
|
|
96
|
+
profileName: string;
|
|
97
|
+
/** 精确版本(不带 ^ ~ 等前缀)。 */
|
|
98
|
+
version: string;
|
|
99
|
+
/** 这条路自己的参数数组;含义随路由而定(见本接口说明)。 */
|
|
100
|
+
pluginArgs: string[];
|
|
101
|
+
/** 执行时限(毫秒):超时终止(命令行终止整棵进程树,管理器请求取消)并按安装失败处理。 */
|
|
102
|
+
timeoutMs: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 适配器看到的环境(小零件:读版本、看环境、上报宿主种类)。
|
|
106
|
+
* 拼安装命令和选执行器路由收归核心(精确版本、强制官方源是政策,不是跑腿)。
|
|
107
|
+
* environmentKind 由适配器探测,顺序固定:有 `desktopProfiles` 即 desktop;
|
|
108
|
+
* 否则有 `pluginManager.installBundle` 且使用范围名恰是命令行明确封禁的 `desktop` 即 desktop-manager;
|
|
109
|
+
* 其余为 cli。探测不出就按 cli 走,跑不通按诚实失败转手工命令。
|
|
110
|
+
*/
|
|
111
|
+
export interface EnvironmentView {
|
|
112
|
+
profileName: string | null;
|
|
113
|
+
environmentKind: EnvironmentKind;
|
|
114
|
+
homeDir: string | null;
|
|
115
|
+
profileDir: string | null;
|
|
116
|
+
installedVersion: string | null;
|
|
117
|
+
packageValid: boolean;
|
|
118
|
+
sourceInstall: boolean;
|
|
119
|
+
blockedReason: BlockedReason | null;
|
|
120
|
+
installationKey: string | null;
|
|
121
|
+
eligible: boolean;
|
|
122
|
+
}
|
|
123
|
+
/** 核心要的最小网络形状(结构化定义,不依赖任何运行时的网络类型)。 */
|
|
124
|
+
export interface MinimalHeaders {
|
|
125
|
+
get(name: string): string | null;
|
|
126
|
+
}
|
|
127
|
+
export interface MinimalResponse {
|
|
128
|
+
ok: boolean;
|
|
129
|
+
/** HTTP 状态码(仅联网取数有;假件不给即省略,调用方不得猜)。 */
|
|
130
|
+
status?: number;
|
|
131
|
+
headers: MinimalHeaders;
|
|
132
|
+
text(): Promise<string>;
|
|
133
|
+
}
|
|
134
|
+
export type FetchImpl = (url: string, init?: {
|
|
135
|
+
headers?: Record<string, string>;
|
|
136
|
+
redirect?: string;
|
|
137
|
+
signal?: unknown;
|
|
138
|
+
}) => Promise<MinimalResponse>;
|
|
139
|
+
/**
|
|
140
|
+
* 插口:时钟和编号从外面灌进来(默认真的、测试用假的)。
|
|
141
|
+
* 核心不直接读硬盘、不直接联网,联网经外面递进来的 fetchImpl 发生一次。
|
|
142
|
+
* 安装用的存取与执行也全是外面给的小零件:任务记哪里、锁怎么加、
|
|
143
|
+
* 备份怎么做、安装命令怎么跑,核心只指挥顺序,不碰硬盘与子进程。
|
|
144
|
+
*
|
|
145
|
+
* 新增注入点位(规格 #591 第 2、4、5 条,全部可选,默认值等于现状):
|
|
146
|
+
* 插件标识、目标包名、官方源、安装时限。不传即走现状,老调用补一个标识即可。
|
|
147
|
+
*/
|
|
148
|
+
export interface UpdatePorts {
|
|
149
|
+
readRunningVersion(): string;
|
|
150
|
+
readInstalled(): EnvironmentView | Promise<EnvironmentView>;
|
|
151
|
+
fetchImpl: FetchImpl;
|
|
152
|
+
now(): number;
|
|
153
|
+
randomId(): string;
|
|
154
|
+
nodeVersion: string;
|
|
155
|
+
checkTimeoutMs?: number;
|
|
156
|
+
confirmationTtlMs?: number;
|
|
157
|
+
/** 插件标识:单例键与日志标识用(规格 #591 第 2、6、8 条)。 */
|
|
158
|
+
pluginId?: string;
|
|
159
|
+
/** 目标包名:要检查更新的那个包是谁(规格 #591 第 4 条,默认等于现状)。 */
|
|
160
|
+
targetPackageName?: string;
|
|
161
|
+
/** 官方源:默认等于现状地址(规格 #591 第 4 条)。 */
|
|
162
|
+
registryUrl?: string;
|
|
163
|
+
/** 版本通道:默认 stable(与旧行为一字不差),显式传 prerelease 才收预发布(#16)。 */
|
|
164
|
+
releaseChannel?: ReleaseChannel;
|
|
165
|
+
/** 安装时限毫秒:默认 15 分钟(规格 #591 第 5 条)。 */
|
|
166
|
+
installTimeoutMs?: number;
|
|
167
|
+
/** 读已落盘的任务(没有为 null;读坏由外面抛错,核心收敛为状态不可用)。 */
|
|
168
|
+
readJob?: () => UpdateJob | null | Promise<UpdateJob | null>;
|
|
169
|
+
/** 写任务(传 null 为清空;写坏由外面抛错)。 */
|
|
170
|
+
writeJob?: (job: UpdateJob | null) => void | Promise<void>;
|
|
171
|
+
/** 抢锁:抢到为 true,被别人占着为 false(同一个使用范围同时只装一个)。 */
|
|
172
|
+
tryAcquireLock?: (lockId: string) => boolean | Promise<boolean>;
|
|
173
|
+
/** 放锁(只放自己抢到的那把)。 */
|
|
174
|
+
releaseLock?: (lockId: string) => void | Promise<void>;
|
|
175
|
+
/** 装前备份使用范围的清单(小清单,不含凭据与整个家目录)。 */
|
|
176
|
+
backupJob?: (job: UpdateJob) => void | Promise<void>;
|
|
177
|
+
/**
|
|
178
|
+
* 真正跑安装(按钮强制官方源、精确版本与 --save-exact)。
|
|
179
|
+
* 核心只把政策要的三件事交给适配器:版本、使用范围名、宿主种类;
|
|
180
|
+
* 适配器必须调 installRecipe 拿配方再执行,不得自己按系统或宿主分支选路由。
|
|
181
|
+
* 测试一律给假的,不真跑。
|
|
182
|
+
*/
|
|
183
|
+
runInstall?: (args: {
|
|
184
|
+
version: string;
|
|
185
|
+
profileName: string | null;
|
|
186
|
+
environmentKind: EnvironmentKind;
|
|
187
|
+
}) => void | Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
/** 对外三个方法(进度不单独给方法,调用方轮询查状态)。 */
|
|
190
|
+
export interface UpdateCore {
|
|
191
|
+
/** 查状态:只读本地,不联网。 */
|
|
192
|
+
status(): Promise<UpdateSnapshot>;
|
|
193
|
+
/** 查新版:联网问一次,成功带回快照加检查凭证(分开交)。 */
|
|
194
|
+
check(): Promise<CheckResult>;
|
|
195
|
+
/**
|
|
196
|
+
* 装更新:拿检查编号加请求编号提交。
|
|
197
|
+
* 只读半程诚实失败(unsupported),安装票再实现。
|
|
198
|
+
*/
|
|
199
|
+
install(args: {
|
|
200
|
+
checkId: string;
|
|
201
|
+
requestId: string;
|
|
202
|
+
}): Promise<UpdateSnapshot>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 模块标识(运行时唯一内容):类型在转译时已擦除,
|
|
206
|
+
* 转译出的 JS 只剩这一行;第二家直接吃 TS 源码。
|
|
207
|
+
*/
|
|
208
|
+
export declare const PORTS_SOURCE = "src/ports.ts";
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** 队列意向默认有效期:10 分钟(与凭证有效期同口径,调用方可覆盖)。 */
|
|
2
|
+
export declare const QUEUE_INTENT_TTL_MS: number;
|
|
3
|
+
/** 正在安装的拥有者(一次只能有一个,与全局锁持有者一一对应)。 */
|
|
4
|
+
export interface QueueOwner {
|
|
5
|
+
pluginId: string;
|
|
6
|
+
jobId: string;
|
|
7
|
+
requestId: string | null;
|
|
8
|
+
targetVersion: string | null;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
}
|
|
11
|
+
/** 排队中的安装意向(尚未开始装,只占位)。 */
|
|
12
|
+
export interface QueuedEntry {
|
|
13
|
+
pluginId: string;
|
|
14
|
+
requestId: string | null;
|
|
15
|
+
targetVersion: string | null;
|
|
16
|
+
enqueuedAt: number;
|
|
17
|
+
}
|
|
18
|
+
/** 队列落盘形状(版本号锁死为 1,收到别的数字按空队列处理,不抛错)。 */
|
|
19
|
+
export interface UpdateQueueState {
|
|
20
|
+
version: 1;
|
|
21
|
+
owner: QueueOwner | null;
|
|
22
|
+
waiting: QueuedEntry[];
|
|
23
|
+
}
|
|
24
|
+
/** 空队列(调用方拿它当起点,落盘文件缺失或损坏时同样回退到它)。 */
|
|
25
|
+
export declare function emptyQueueState(): UpdateQueueState;
|
|
26
|
+
/** 把外面读到的值验成可用的队列:错了不抛错,直接回空队列(队列坏了不能挡安装)。 */
|
|
27
|
+
export declare function normalizeQueueState(raw: unknown): UpdateQueueState;
|
|
28
|
+
/** 丢弃过期的 waiting 意向(owner 不过期——它由全局锁与安装时限看守,不由这里清)。 */
|
|
29
|
+
export declare function pruneExpiredIntents(state: UpdateQueueState, nowMs: number, ttlMs?: number): UpdateQueueState;
|
|
30
|
+
/**
|
|
31
|
+
* 占位:同一 (pluginId, requestId) 幂等占一位,已在即返回旧位置不挪位。
|
|
32
|
+
* 位置口径:0 = 已是拥有者,1..n = waiting 中的顺位(队首为 1),与 visibleQueueFor 的
|
|
33
|
+
* position 同口径。targetVersion 仅作展示用,不参与身份判定;已在时不覆盖旧条目。
|
|
34
|
+
*/
|
|
35
|
+
export declare function enqueueInQueue(state: UpdateQueueState, entry: {
|
|
36
|
+
pluginId: string;
|
|
37
|
+
requestId?: string | null;
|
|
38
|
+
targetVersion?: string | null;
|
|
39
|
+
enqueuedAt: number;
|
|
40
|
+
}): {
|
|
41
|
+
state: UpdateQueueState;
|
|
42
|
+
position: number | null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* 取消排队意向:只能取消自己的 waiting 条目(pluginId 必须对上),owner 不经这里取消。
|
|
46
|
+
* 对不上(他人的条目、owner、查无此人)返回 removed: false 且状态原样。
|
|
47
|
+
*/
|
|
48
|
+
export declare function cancelEnqueuedInQueue(state: UpdateQueueState, pluginId: string, requestId: string | null): {
|
|
49
|
+
state: UpdateQueueState;
|
|
50
|
+
removed: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** 拥有者收尾:只有 (pluginId, jobId) 双对上才清掉,对不上原样(不替别人摘牌)。 */
|
|
53
|
+
export declare function releaseOwnerInQueue(state: UpdateQueueState, pluginId: string, jobId: string): {
|
|
54
|
+
state: UpdateQueueState;
|
|
55
|
+
released: boolean;
|
|
56
|
+
};
|
|
57
|
+
/** 把队首意向提为拥有者(调用方抢到全局锁后记账用;队空或信息不全返回 promoted: false)。 */
|
|
58
|
+
export declare function promoteHeadToOwner(state: UpdateQueueState, args: {
|
|
59
|
+
jobId: string;
|
|
60
|
+
startedAt: number;
|
|
61
|
+
}): {
|
|
62
|
+
state: UpdateQueueState;
|
|
63
|
+
promoted: boolean;
|
|
64
|
+
};
|
|
65
|
+
/** 直接记拥有者(抢到全局锁但 waiting 里没有自己意向时的兜底记账;已有 owner 不覆盖)。 */
|
|
66
|
+
export declare function setOwnerIfFree(state: UpdateQueueState, owner: {
|
|
67
|
+
pluginId: string;
|
|
68
|
+
jobId: string;
|
|
69
|
+
requestId?: string | null;
|
|
70
|
+
targetVersion?: string | null;
|
|
71
|
+
startedAt: number;
|
|
72
|
+
}): {
|
|
73
|
+
state: UpdateQueueState;
|
|
74
|
+
set: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* 查位置:0 = 正拥有全局锁在装,1..n = waiting 顺位(队首为 1),null = 不在队列里。
|
|
78
|
+
* 面板拿它展示“前方还有 N 个”,不展开他人明细也能排得明白。
|
|
79
|
+
*/
|
|
80
|
+
export declare function queuePositionOf(state: UpdateQueueState, pluginId: string, requestId: string | null): number | null;
|
|
81
|
+
/** 队首是不是我(公平门:非队首不许抢锁;队空时人人都是队首)。 */
|
|
82
|
+
export declare function isHeadOfQueue(state: UpdateQueueState, pluginId: string, requestId: string | null): boolean;
|
|
83
|
+
/** 全局是否正忙(有人拥有锁在装)。 */
|
|
84
|
+
export declare function isQueueBusy(state: UpdateQueueState): boolean;
|
|
85
|
+
export interface VisibleQueueOwner {
|
|
86
|
+
/** 他人拥有时仅露 busy 占位(pluginId 为 null),不露标识;自己拥有时给全量。 */
|
|
87
|
+
pluginId: string | null;
|
|
88
|
+
jobId?: string;
|
|
89
|
+
requestId?: string | null;
|
|
90
|
+
targetVersion?: string | null;
|
|
91
|
+
startedAt?: number;
|
|
92
|
+
busy: true;
|
|
93
|
+
}
|
|
94
|
+
export interface VisibleQueue {
|
|
95
|
+
busy: boolean;
|
|
96
|
+
/** 未隐藏时为全量 owner;隐藏他人时他人 owner 折成 {pluginId: null, busy: true};空闲为 null。 */
|
|
97
|
+
owner: QueueOwner | VisibleQueueOwner | null;
|
|
98
|
+
/** 未隐藏时为全 waiting;隐藏时仅留查看者自己的。 */
|
|
99
|
+
waiting: QueuedEntry[];
|
|
100
|
+
/** 查看者在全队列中的位置(0 = 在装,1..n = 顺位,null = 不在队列),隐藏时同样给(只给数不给明细)。 */
|
|
101
|
+
position: number | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 面板可见性开关:包提供全面功能,消费者传参决定是否看他人项。
|
|
105
|
+
* - showOthers=true:owner + waiting 全量。
|
|
106
|
+
* - showOthers=false(默认):waiting 仅留自己的;owner 是自己的给全量,是他人的折成
|
|
107
|
+
* busy 占位(能显示“前方有其他插件在安装”,但看不到是哪一家)。
|
|
108
|
+
*/
|
|
109
|
+
export declare function visibleQueueFor(state: UpdateQueueState, viewerPluginId: string, showOthers?: boolean, requestId?: string | null): VisibleQueue;
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/queue.ts
|
|
2
|
+
const QUEUE_INTENT_TTL_MS = 10 * 6e4;
|
|
3
|
+
function emptyQueueState() {
|
|
4
|
+
return { version: 1, owner: null, waiting: [] };
|
|
5
|
+
}
|
|
6
|
+
function isNonEmptyString(value) {
|
|
7
|
+
return typeof value === "string" && value.length > 0;
|
|
8
|
+
}
|
|
9
|
+
function asMillis(value, fallback) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
11
|
+
}
|
|
12
|
+
function normalizeQueueState(raw) {
|
|
13
|
+
try {
|
|
14
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return emptyQueueState();
|
|
15
|
+
const input = raw;
|
|
16
|
+
if (input["version"] !== 1) return emptyQueueState();
|
|
17
|
+
let owner = null;
|
|
18
|
+
const rawOwner = input["owner"];
|
|
19
|
+
if (rawOwner && typeof rawOwner === "object" && !Array.isArray(rawOwner)) {
|
|
20
|
+
const o = rawOwner;
|
|
21
|
+
if (isNonEmptyString(o["pluginId"]) && isNonEmptyString(o["jobId"])) {
|
|
22
|
+
owner = {
|
|
23
|
+
pluginId: o["pluginId"],
|
|
24
|
+
jobId: o["jobId"],
|
|
25
|
+
requestId: typeof o["requestId"] === "string" ? o["requestId"] : null,
|
|
26
|
+
targetVersion: typeof o["targetVersion"] === "string" ? o["targetVersion"] : null,
|
|
27
|
+
startedAt: asMillis(o["startedAt"], 0)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const waiting = [];
|
|
32
|
+
const rawWaiting = input["waiting"];
|
|
33
|
+
if (Array.isArray(rawWaiting)) {
|
|
34
|
+
for (const item of rawWaiting) {
|
|
35
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
36
|
+
const e = item;
|
|
37
|
+
if (!isNonEmptyString(e["pluginId"])) continue;
|
|
38
|
+
waiting.push({
|
|
39
|
+
pluginId: e["pluginId"],
|
|
40
|
+
requestId: typeof e["requestId"] === "string" ? e["requestId"] : null,
|
|
41
|
+
targetVersion: typeof e["targetVersion"] === "string" ? e["targetVersion"] : null,
|
|
42
|
+
enqueuedAt: asMillis(e["enqueuedAt"], 0)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { version: 1, owner, waiting };
|
|
47
|
+
} catch {
|
|
48
|
+
return emptyQueueState();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function sameIntent(a, b) {
|
|
52
|
+
return a.pluginId === b.pluginId && (a.requestId ?? null) === (b.requestId ?? null);
|
|
53
|
+
}
|
|
54
|
+
function pruneExpiredIntents(state, nowMs, ttlMs = QUEUE_INTENT_TTL_MS) {
|
|
55
|
+
const now = asMillis(nowMs, 0);
|
|
56
|
+
const ttl = typeof ttlMs === "number" && Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : QUEUE_INTENT_TTL_MS;
|
|
57
|
+
const waiting = state.waiting.filter((e) => now - asMillis(e.enqueuedAt, now) < ttl);
|
|
58
|
+
if (waiting.length === state.waiting.length) return state;
|
|
59
|
+
return { version: 1, owner: state.owner, waiting };
|
|
60
|
+
}
|
|
61
|
+
function enqueueInQueue(state, entry) {
|
|
62
|
+
const base = normalizeQueueState(state);
|
|
63
|
+
if (!isNonEmptyString(entry.pluginId)) return { state: base, position: null };
|
|
64
|
+
const key = { pluginId: entry.pluginId, requestId: entry.requestId ?? null };
|
|
65
|
+
if (base.owner && sameIntent(base.owner, key)) return { state: base, position: 0 };
|
|
66
|
+
const at = base.waiting.findIndex((e) => sameIntent(e, key));
|
|
67
|
+
if (at >= 0) return { state: base, position: at + 1 };
|
|
68
|
+
const next = {
|
|
69
|
+
pluginId: entry.pluginId,
|
|
70
|
+
requestId: entry.requestId ?? null,
|
|
71
|
+
targetVersion: typeof entry.targetVersion === "string" ? entry.targetVersion : null,
|
|
72
|
+
enqueuedAt: asMillis(entry.enqueuedAt, 0)
|
|
73
|
+
};
|
|
74
|
+
const waiting = [...base.waiting, next];
|
|
75
|
+
return { state: { version: 1, owner: base.owner, waiting }, position: waiting.length };
|
|
76
|
+
}
|
|
77
|
+
function cancelEnqueuedInQueue(state, pluginId, requestId) {
|
|
78
|
+
const base = normalizeQueueState(state);
|
|
79
|
+
if (!isNonEmptyString(pluginId)) return { state: base, removed: false };
|
|
80
|
+
const key = { pluginId, requestId: requestId ?? null };
|
|
81
|
+
if (base.owner && sameIntent(base.owner, key)) return { state: base, removed: false };
|
|
82
|
+
const at = base.waiting.findIndex((e) => sameIntent(e, key));
|
|
83
|
+
if (at < 0) return { state: base, removed: false };
|
|
84
|
+
const waiting = [...base.waiting.slice(0, at), ...base.waiting.slice(at + 1)];
|
|
85
|
+
return { state: { version: 1, owner: base.owner, waiting }, removed: true };
|
|
86
|
+
}
|
|
87
|
+
function releaseOwnerInQueue(state, pluginId, jobId) {
|
|
88
|
+
const base = normalizeQueueState(state);
|
|
89
|
+
if (!isNonEmptyString(pluginId) || !isNonEmptyString(jobId)) return { state: base, released: false };
|
|
90
|
+
if (!base.owner || base.owner.pluginId !== pluginId || base.owner.jobId !== jobId) {
|
|
91
|
+
return { state: base, released: false };
|
|
92
|
+
}
|
|
93
|
+
return { state: { version: 1, owner: null, waiting: base.waiting }, released: true };
|
|
94
|
+
}
|
|
95
|
+
function promoteHeadToOwner(state, args) {
|
|
96
|
+
const base = normalizeQueueState(state);
|
|
97
|
+
if (base.owner || base.waiting.length === 0) return { state: base, promoted: false };
|
|
98
|
+
if (!isNonEmptyString(args.jobId)) return { state: base, promoted: false };
|
|
99
|
+
const [head, ...rest] = base.waiting;
|
|
100
|
+
const owner = {
|
|
101
|
+
pluginId: head.pluginId,
|
|
102
|
+
jobId: args.jobId,
|
|
103
|
+
requestId: head.requestId,
|
|
104
|
+
targetVersion: head.targetVersion,
|
|
105
|
+
startedAt: asMillis(args.startedAt, 0)
|
|
106
|
+
};
|
|
107
|
+
return { state: { version: 1, owner, waiting: rest }, promoted: true };
|
|
108
|
+
}
|
|
109
|
+
function setOwnerIfFree(state, owner) {
|
|
110
|
+
const base = normalizeQueueState(state);
|
|
111
|
+
if (base.owner) return { state: base, set: false };
|
|
112
|
+
if (!isNonEmptyString(owner.pluginId) || !isNonEmptyString(owner.jobId)) return { state: base, set: false };
|
|
113
|
+
return {
|
|
114
|
+
state: {
|
|
115
|
+
version: 1,
|
|
116
|
+
owner: {
|
|
117
|
+
pluginId: owner.pluginId,
|
|
118
|
+
jobId: owner.jobId,
|
|
119
|
+
requestId: owner.requestId ?? null,
|
|
120
|
+
targetVersion: typeof owner.targetVersion === "string" ? owner.targetVersion : null,
|
|
121
|
+
startedAt: asMillis(owner.startedAt, 0)
|
|
122
|
+
},
|
|
123
|
+
// 抢到锁即消费自己的队首意向(若有),不留僵尸占位。
|
|
124
|
+
waiting: base.waiting.filter((e) => !sameIntent(e, { pluginId: owner.pluginId, requestId: owner.requestId ?? null }))
|
|
125
|
+
},
|
|
126
|
+
set: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function queuePositionOf(state, pluginId, requestId) {
|
|
130
|
+
const base = normalizeQueueState(state);
|
|
131
|
+
if (!isNonEmptyString(pluginId)) return null;
|
|
132
|
+
const key = { pluginId, requestId: requestId ?? null };
|
|
133
|
+
if (base.owner && sameIntent(base.owner, key)) return 0;
|
|
134
|
+
const at = base.waiting.findIndex((e) => sameIntent(e, key));
|
|
135
|
+
return at >= 0 ? at + 1 : null;
|
|
136
|
+
}
|
|
137
|
+
function isHeadOfQueue(state, pluginId, requestId) {
|
|
138
|
+
const base = normalizeQueueState(state);
|
|
139
|
+
if (!isNonEmptyString(pluginId)) return false;
|
|
140
|
+
if (base.waiting.length === 0) return true;
|
|
141
|
+
const head = base.waiting[0];
|
|
142
|
+
return head.pluginId === pluginId && (head.requestId ?? null) === (requestId ?? null);
|
|
143
|
+
}
|
|
144
|
+
function isQueueBusy(state) {
|
|
145
|
+
return normalizeQueueState(state).owner !== null;
|
|
146
|
+
}
|
|
147
|
+
function visibleQueueFor(state, viewerPluginId, showOthers = false, requestId) {
|
|
148
|
+
const base = normalizeQueueState(state);
|
|
149
|
+
const rid = requestId === void 0 ? derivedRequestId(base, viewerPluginId) : requestId ?? null;
|
|
150
|
+
const position = isNonEmptyString(viewerPluginId) ? queuePositionOf(base, viewerPluginId, rid) : null;
|
|
151
|
+
if (showOthers === true) {
|
|
152
|
+
return { busy: base.owner !== null, owner: base.owner, waiting: [...base.waiting], position };
|
|
153
|
+
}
|
|
154
|
+
const mine = isNonEmptyString(viewerPluginId) ? base.waiting.filter((e) => e.pluginId === viewerPluginId) : [];
|
|
155
|
+
let owner = null;
|
|
156
|
+
if (base.owner) {
|
|
157
|
+
if (base.owner.pluginId === viewerPluginId) owner = base.owner;
|
|
158
|
+
else owner = { pluginId: null, busy: true };
|
|
159
|
+
}
|
|
160
|
+
return { busy: base.owner !== null, owner, waiting: mine, position };
|
|
161
|
+
}
|
|
162
|
+
function derivedRequestId(state, viewerPluginId) {
|
|
163
|
+
const mine = state.waiting.filter((e) => e.pluginId === viewerPluginId);
|
|
164
|
+
if (state.owner && state.owner.pluginId === viewerPluginId) return state.owner.requestId;
|
|
165
|
+
if (mine.length === 0) return null;
|
|
166
|
+
return mine[0].requestId;
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
QUEUE_INTENT_TTL_MS,
|
|
170
|
+
cancelEnqueuedInQueue,
|
|
171
|
+
emptyQueueState,
|
|
172
|
+
enqueueInQueue,
|
|
173
|
+
isHeadOfQueue,
|
|
174
|
+
isQueueBusy,
|
|
175
|
+
normalizeQueueState,
|
|
176
|
+
promoteHeadToOwner,
|
|
177
|
+
pruneExpiredIntents,
|
|
178
|
+
queuePositionOf,
|
|
179
|
+
releaseOwnerInQueue,
|
|
180
|
+
setOwnerIfFree,
|
|
181
|
+
visibleQueueFor
|
|
182
|
+
};
|
package/dist/reader.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/reader.ts —— 把本机真实情况翻译成更新核心要的零件(读取器)。
|
|
3
|
+
*
|
|
4
|
+
* 由 src/host/updateReader.js 改写拎入:只翻译不决定。相对旧实现的增量(规格 #591):
|
|
5
|
+
* 1. 插件标识必填(第 2 条):非空且不含路径分隔符;环境指纹强制含插件标识(第 6 条单例隔离)。
|
|
6
|
+
* 2. 三处注入默认现状(第 4 条):目标包名、官方源、目录根经配置注入,默认值等于现状。
|
|
7
|
+
* 3. 时间可调(第 5 条):联网超时、凭证有效期、安装时限经配置注入,默认值等于现状。
|
|
8
|
+
* 4. 包完好判定看目标包名(默认旧包名,默认行为零变化)。
|
|
9
|
+
*/
|
|
10
|
+
import type { EnvironmentKind, EnvironmentView, FetchImpl, ReleaseChannel, UpdateCore, UpdateJob } from './ports.js';
|
|
11
|
+
/** 从起点文件向上找到名字对得上的包(装的是源码还是装好的包都认)。 */
|
|
12
|
+
export declare function containingPackage(filename: string, name: string): Promise<{
|
|
13
|
+
directory: string;
|
|
14
|
+
manifest: Record<string, unknown>;
|
|
15
|
+
contents: string;
|
|
16
|
+
} | null>;
|
|
17
|
+
export interface ResolveTargetPackageOptions {
|
|
18
|
+
/** 显式目标包目录:给了就不走任何自动解析(hoisted、多副本、开发态链接等场景的逃生口)。 */
|
|
19
|
+
targetPackageDir?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 按包名解析目标包(issue #3 治本:不再拿“本包自己住在哪”回答“要更新的包在哪”)。
|
|
23
|
+
*
|
|
24
|
+
* 顺序固定:
|
|
25
|
+
* 1. 显式目录优先(给了就不走自动解析);
|
|
26
|
+
* 2. `createRequire(base).resolve(name + '/package.json')` 直解清单;
|
|
27
|
+
* 3. `createRequire(base).resolve(name)` 取入口,再用 `containingPackage(入口)` 向上找清单;
|
|
28
|
+
* 4. 手工 sibling 步行(绕过 `exports` 映射:逐级找 `node_modules/<name>/package.json`;
|
|
29
|
+
* 第 2、3 步受 `exports` 约束,对“契约完整但未导出 `.` 与 `./package.json`”的包会失败,
|
|
30
|
+
* 这一步按文件系统直读,保证普通依赖形态一定命中);
|
|
31
|
+
* 5. 退回自锚定 `containingPackage(自身文件)`(vendor 在目标包内部的老形态)。
|
|
32
|
+
*
|
|
33
|
+
* 全失败返回 null,调用方诚实失败,不猜。
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveTargetPackage(targetName: string, opts?: ResolveTargetPackageOptions): Promise<{
|
|
36
|
+
directory: string;
|
|
37
|
+
manifest: Record<string, unknown>;
|
|
38
|
+
contents: string;
|
|
39
|
+
} | null>;
|
|
40
|
+
/** 使用范围名的唯一取法:调用方给了就用它,否则取使用范围目录的最后一段(宿主探测与读环境共用)。 */
|
|
41
|
+
export declare function resolveProfileName(profileName: string | null | undefined, profileDir: string): string;
|
|
42
|
+
/** 使用范围名是否合法(纯谓词,导出供验证与面板复用)。 */
|
|
43
|
+
export declare function profileNameValid(name: unknown): boolean;
|
|
44
|
+
/** 依赖写法是否像从源装的(纯谓词,导出供验证与面板复用)。 */
|
|
45
|
+
export declare function registrySpec(spec: unknown): boolean;
|
|
46
|
+
/** 家目录默认值:环境变量优先,支持 ~ 写法(导出给调用方取默认值用)。 */
|
|
47
|
+
export declare function defaultHomeDir(env: Record<string, string | undefined>, osHome: string): string;
|
|
48
|
+
export interface UpdateReaderOptions {
|
|
49
|
+
runningVersion: string;
|
|
50
|
+
profileDir: string;
|
|
51
|
+
pluginId: string;
|
|
52
|
+
profileName?: string;
|
|
53
|
+
homeDir?: string;
|
|
54
|
+
/** 显式目标包目录:优先于按包名自动解析(hoisted、多副本、开发态链接等场景的逃生口)。 */
|
|
55
|
+
targetPackageDir?: string;
|
|
56
|
+
env?: Record<string, string | undefined>;
|
|
57
|
+
osHome?: string;
|
|
58
|
+
fetchImpl?: FetchImpl;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
randomId?: () => string;
|
|
61
|
+
nodeVersion?: string;
|
|
62
|
+
environmentKind?: EnvironmentKind;
|
|
63
|
+
targetPackageName?: string;
|
|
64
|
+
registryUrl?: string;
|
|
65
|
+
checkTimeoutMs?: number;
|
|
66
|
+
confirmationTtlMs?: number;
|
|
67
|
+
/** 版本通道:默认 stable,显式传 prerelease 才收预发布(#16,加法扩展)。 */
|
|
68
|
+
releaseChannel?: ReleaseChannel;
|
|
69
|
+
readInstalled?: () => EnvironmentView | Promise<EnvironmentView>;
|
|
70
|
+
readJob?: () => UpdateJob | null | Promise<UpdateJob | null>;
|
|
71
|
+
writeJob?: (job: UpdateJob | null) => void | Promise<void>;
|
|
72
|
+
tryAcquireLock?: (lockId: string) => boolean | Promise<boolean>;
|
|
73
|
+
releaseLock?: (lockId: string) => void | Promise<void>;
|
|
74
|
+
backupJob?: (job: UpdateJob) => void | Promise<void>;
|
|
75
|
+
runInstall?: (args: {
|
|
76
|
+
version: string;
|
|
77
|
+
profileName: string | null;
|
|
78
|
+
environmentKind: EnvironmentKind;
|
|
79
|
+
}) => void | Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/** 建更新读取器:运行版本、使用范围目录、插件标识三者必填,其余不给用本机默认。 */
|
|
82
|
+
export declare function createUpdateReader(options: UpdateReaderOptions): UpdateCore & {
|
|
83
|
+
readEnv: () => Promise<EnvironmentView>;
|
|
84
|
+
};
|
package/dist/reader.js
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { assertPluginId } from "./config.js";
|
|
9
|
-
import { createUpdateCore, validVersion } from "./service.js";
|
|
9
|
+
import { createUpdateCore, isVersionAllowedInChannel, validVersion } from "./service.js";
|
|
10
10
|
const LOCK_FILES = ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package-lock.json"];
|
|
11
11
|
function inside(directory, filename) {
|
|
12
12
|
const suffix = relative(directory, filename);
|
|
@@ -97,8 +97,8 @@ function profileNameValid(name) {
|
|
|
97
97
|
function registrySpec(spec) {
|
|
98
98
|
return typeof spec === "string" && spec.trim().length > 0 && (validVersion(spec.trim()) || /^[~^>=< ]*[0-9x*][0-9x*./\-_ |~^>=<]*$/u.test(spec.trim()) || /^[A-Za-z][A-Za-z0-9._-]*$/u.test(spec.trim()));
|
|
99
99
|
}
|
|
100
|
-
async function validPackage(pkg, targetName) {
|
|
101
|
-
if (!pkg || pkg.manifest?.name !== targetName || !
|
|
100
|
+
async function validPackage(pkg, targetName, channel = "stable") {
|
|
101
|
+
if (!pkg || pkg.manifest?.name !== targetName || !isVersionAllowedInChannel(pkg.manifest.version, channel)) return false;
|
|
102
102
|
const main = pkg.manifest.main;
|
|
103
103
|
const exportsField = pkg.manifest.exports;
|
|
104
104
|
const dshField = pkg.manifest.dsh;
|
|
@@ -153,6 +153,7 @@ function createUpdateReader(options) {
|
|
|
153
153
|
const environmentKind = options.environmentKind ?? "cli";
|
|
154
154
|
const targetPackageName = options.targetPackageName ?? "dsh-mattpocock-skills-deck";
|
|
155
155
|
const registryUrl = options.registryUrl ?? "https://registry.npmjs.org/";
|
|
156
|
+
const releaseChannel = options.releaseChannel === "prerelease" ? "prerelease" : "stable";
|
|
156
157
|
const loadedPackage = resolveTargetPackage(
|
|
157
158
|
targetPackageName,
|
|
158
159
|
options.targetPackageDir ? { targetPackageDir: options.targetPackageDir } : {}
|
|
@@ -200,7 +201,7 @@ function createUpdateReader(options) {
|
|
|
200
201
|
const deps = profile.manifest.dependencies ?? {};
|
|
201
202
|
result.sourceInstall = !registrySpec(deps[targetPackageName]) || !inside(join(profileDir, "node_modules"), installed.directory);
|
|
202
203
|
result.installedVersion = typeof installed.manifest.version === "string" ? installed.manifest.version : null;
|
|
203
|
-
result.packageValid = await validPackage(installed, targetPackageName);
|
|
204
|
+
result.packageValid = await validPackage(installed, targetPackageName, releaseChannel);
|
|
204
205
|
const loaded = await loadedPackage;
|
|
205
206
|
const loadedUnresolvable = loaded === null;
|
|
206
207
|
const identity = `${homeDir}\0${profileDir}\0${profileName}\0${pluginId}`;
|
|
@@ -234,6 +235,7 @@ function createUpdateReader(options) {
|
|
|
234
235
|
confirmationTtlMs: options.confirmationTtlMs,
|
|
235
236
|
targetPackageName,
|
|
236
237
|
registryUrl,
|
|
238
|
+
releaseChannel,
|
|
237
239
|
readJob: options.readJob,
|
|
238
240
|
writeJob: options.writeJob,
|
|
239
241
|
tryAcquireLock: options.tryAcquireLock,
|