@morehao/dsh-plugin-kit 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/http.d.ts +28 -0
- package/dist/http.js +51 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +179 -0
- package/dist/profile-C-eyP5F6.d.ts +44 -0
- package/dist/update-client.d.ts +57 -0
- package/dist/update-client.js +86 -0
- package/dist/update.d.ts +87 -0
- package/dist/update.js +459 -0
- package/package.json +67 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 morehao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @morehao/dsh-plugin-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness(dsh)插件共用的工具包。当前提供三件事:
|
|
4
|
+
|
|
5
|
+
- **插件自更新引擎**(`./update`):查 npm 最新版 → 判断能不能一键更新 → 经宿主 `pluginManager` 重装自己。
|
|
6
|
+
- **headless 客户端控制器**(`./update/client`):检查/更新的状态机与单飞,UI 由各插件自己渲染。
|
|
7
|
+
- **HTTP 小工具**(`./http`):自定义头鉴权、`no-store` JSON、有上限的 JSON body。
|
|
8
|
+
|
|
9
|
+
设计约束:**零 `@deepseek-ai/*` 运行时依赖**,宿主能力(`pluginManager` / `profileContext`)全部由调用方注入;进程相关的策略(比如用哪个包管理器查版本)也由插件自己决定,本包不替你 spawn。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @morehao/dsh-plugin-kit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 一键更新(host)
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createPluginUpdater } from '@morehao/dsh-plugin-kit/update';
|
|
21
|
+
|
|
22
|
+
const updater = createPluginUpdater({
|
|
23
|
+
meta: { name: pkg.name, version: pkg.version, ...(pkg.repository ? { repository: repoUrl(pkg.repository) } : {}) },
|
|
24
|
+
getPluginManager: () => ctx.get('pluginManager'),
|
|
25
|
+
getProfile: () => ctx.get('profileContext'),
|
|
26
|
+
getProfileName: () => ctx.get('profileContext')?.name,
|
|
27
|
+
// 可选:用 profile 的包管理器查版本(继承 .npmrc 的镜像 / 私源 / token)
|
|
28
|
+
runPnpmView: async (spec) => {
|
|
29
|
+
const profile = ctx.get('profileContext');
|
|
30
|
+
const result = await execCapture(profile.packageManager?.command ?? 'pnpm', [...(profile.packageManager?.args ?? []), 'view', spec, 'version', '--json'], {
|
|
31
|
+
cwd: profile.dir,
|
|
32
|
+
env: sanitizedEnv(process.env),
|
|
33
|
+
timeoutMs: 8_000,
|
|
34
|
+
});
|
|
35
|
+
if (result.code !== 0) return null;
|
|
36
|
+
const value = JSON.parse(result.stdout.trim() || 'null');
|
|
37
|
+
return typeof (Array.isArray(value) ? value.at(-1) : value) === 'string' ? (Array.isArray(value) ? value.at(-1) : value) : null;
|
|
38
|
+
},
|
|
39
|
+
logger: (event, detail) => log(event, detail),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const view = await updater.check({ fresh: true });
|
|
43
|
+
// { packageName, currentVersion, latestVersion, updateAvailable, checkFailed,
|
|
44
|
+
// canApply, installKind, installSpec?, manualCommand, applying, checkedAt }
|
|
45
|
+
|
|
46
|
+
const result = await updater.apply(); // 需要先 check();只接受刚查到的版本
|
|
47
|
+
// { application: 'applied' | 'restart-required' | 'overridden', restartRequired, updatedVersion, installKind, steps }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`apply()` 的行为与边界:
|
|
51
|
+
|
|
52
|
+
- 只安装 **registry 上刚查到的 latest**,不接受调用方随手给的 spec(防注入)。
|
|
53
|
+
- `link:` / `file:` / `workspace:` 安装一律拒绝(`LINKED_INSTALL`),只给 `manualCommand`——避免把开发链接换成 npm 版。
|
|
54
|
+
- `installBundle` 返回 `already-installed` 时走 `removeBundle → installBundle`;remove 之后装失败会尝试装回原版本,仍失败报 `ROLLBACK_FAILED`。
|
|
55
|
+
- 单飞:同一实例同时只允许一次 `apply()`。
|
|
56
|
+
- 没有 `pluginManager.installBundle` 时报 `NO_MANAGER`,界面应展示 `manualCommand`。
|
|
57
|
+
|
|
58
|
+
错误码(`PluginUpdateError.code`):`NO_MANAGER` `LINKED_INSTALL` `NOT_CHECKED` `NO_UPDATE` `INVALID_VERSION` `APPLY_IN_PROGRESS` `INSTALL_FAILED` `REMOVE_FAILED` `ROLLBACK_FAILED` `UNSUPPORTED_RESULT`。
|
|
59
|
+
|
|
60
|
+
固定事件名(便于更新把插件自己卸载后对账):
|
|
61
|
+
|
|
62
|
+
| 事件 | 时机 |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `plugin.update.latest` / `plugin.update.latest.failed` | 查到(或查不到)最新版 |
|
|
65
|
+
| `plugin.update.check` | 一次检查完成 |
|
|
66
|
+
| `plugin.update.apply.start` | 开始安装 |
|
|
67
|
+
| `plugin.update.apply` | 安装成功(带 `application` / `restartRequired`) |
|
|
68
|
+
| `plugin.update.apply.failed` | 安装失败(带 `code` / `steps`) |
|
|
69
|
+
|
|
70
|
+
## 客户端控制器(headless)
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createUpdateController } from '@morehao/dsh-plugin-kit/update/client';
|
|
74
|
+
|
|
75
|
+
const controller = createUpdateController({
|
|
76
|
+
request: {
|
|
77
|
+
check: ({ fresh }) => api.getUpdate(fresh), // 返回 UpdateClientView
|
|
78
|
+
apply: () => api.applyUpdate(), // 返回 { application, restartRequired? }
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
controller.subscribe((state) => render(state));
|
|
82
|
+
await controller.check();
|
|
83
|
+
await controller.apply();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
状态:`idle → checking → available | current | failed`;`available → applying → applied | restart-required | failed`。
|
|
87
|
+
控制器不做 i18n、不引 React:错误码(`CHECK_FAILED` / `NO_UPDATE` / `NO_APPLY` 或宿主抛的 code)交给插件翻译。
|
|
88
|
+
|
|
89
|
+
## HTTP 小工具
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { hasHeader, readJsonBody, sendJson } from '@morehao/dsh-plugin-kit/http';
|
|
93
|
+
|
|
94
|
+
if (!hasHeader(request, 'x-my-plugin')) return sendJson(response, 403, { ok: false, code: 'FORBIDDEN' });
|
|
95
|
+
const body = await readJsonBody(request, { maxBytes: 512 * 1024 });
|
|
96
|
+
sendJson(response, 200, { ok: true });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 版本与兼容
|
|
100
|
+
|
|
101
|
+
- Node `^22.20.0 || >=24.0.0`,纯 ESM。
|
|
102
|
+
- `./update` 与 `./http` 走 host 侧;`./update/client` 只含状态机,不 import node 内置模块(客户端产物只允许 react)。
|
|
103
|
+
- 宿主服务形状按结构化类型处理(`PluginManagerLike` / `ProfileLike`),宿主版本变化不会把插件一起拖下去。
|
|
104
|
+
|
|
105
|
+
## 开发
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install
|
|
109
|
+
npm run check # typecheck + build + 24 条离线测试(假 fetch / 假 pluginManager,不联网、不真装)
|
|
110
|
+
```
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 小工具:三个 web 插件共用的「自定义头鉴权 + no-store JSON + 有上限的 JSON body」。
|
|
3
|
+
*
|
|
4
|
+
* 只依赖结构化形状(不 import node:http 类型),宿主的 request/response 直接可用;
|
|
5
|
+
* 鉴权头名称由调用方给,避免把某一个插件的约定强加给其他插件。
|
|
6
|
+
*/
|
|
7
|
+
interface RequestLike {
|
|
8
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
9
|
+
}
|
|
10
|
+
interface ResponseLike {
|
|
11
|
+
writeHead(status: number, headers: Record<string, string | number>): unknown;
|
|
12
|
+
end(body?: string): unknown;
|
|
13
|
+
}
|
|
14
|
+
type HttpErrorCode = 'PAYLOAD_TOO_LARGE' | 'INVALID_JSON';
|
|
15
|
+
declare class HttpError extends Error {
|
|
16
|
+
readonly code: HttpErrorCode;
|
|
17
|
+
constructor(code: HttpErrorCode, message: string);
|
|
18
|
+
}
|
|
19
|
+
/** 请求头精确等于 `value`(默认 `1`)才算通过:跨源请求要先过预检,顺带挡 CSRF。 */
|
|
20
|
+
declare function hasHeader(request: RequestLike, header: string, value?: string): boolean;
|
|
21
|
+
declare function sendJson(response: ResponseLike, status: number, payload: unknown): void;
|
|
22
|
+
interface ReadJsonOptions {
|
|
23
|
+
maxBytes?: number;
|
|
24
|
+
}
|
|
25
|
+
/** 读一个 JSON 对象 body;超过上限抛 PAYLOAD_TOO_LARGE,非对象/非法 JSON 抛 INVALID_JSON。 */
|
|
26
|
+
declare function readJsonBody(request: AsyncIterable<Uint8Array | string>, options?: ReadJsonOptions): Promise<Record<string, unknown>>;
|
|
27
|
+
|
|
28
|
+
export { HttpError, type HttpErrorCode, type ReadJsonOptions, type RequestLike, type ResponseLike, hasHeader, readJsonBody, sendJson };
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
var HttpError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "HttpError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function hasHeader(request, header, value = "1") {
|
|
11
|
+
return request.headers?.[header.toLowerCase()] === value;
|
|
12
|
+
}
|
|
13
|
+
function sendJson(response, status, payload) {
|
|
14
|
+
const body = JSON.stringify(payload);
|
|
15
|
+
response.writeHead(status, {
|
|
16
|
+
"content-type": "application/json; charset=utf-8",
|
|
17
|
+
"cache-control": "no-store",
|
|
18
|
+
"content-length": Buffer.byteLength(body)
|
|
19
|
+
});
|
|
20
|
+
response.end(body);
|
|
21
|
+
}
|
|
22
|
+
async function readJsonBody(request, options = {}) {
|
|
23
|
+
const maxBytes = options.maxBytes ?? 512 * 1024;
|
|
24
|
+
const chunks = [];
|
|
25
|
+
let size = 0;
|
|
26
|
+
for await (const chunk of request) {
|
|
27
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
|
|
28
|
+
size += buffer.length;
|
|
29
|
+
if (size > maxBytes) throw new HttpError("PAYLOAD_TOO_LARGE", "\u8BF7\u6C42\u4F53\u8FC7\u5927");
|
|
30
|
+
chunks.push(buffer);
|
|
31
|
+
}
|
|
32
|
+
if (chunks.length === 0) return {};
|
|
33
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
34
|
+
if (text === "") return {};
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(text);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new HttpError("INVALID_JSON", `\u8BF7\u6C42\u4F53\u4E0D\u662F JSON \u5BF9\u8C61\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
42
|
+
throw new HttpError("INVALID_JSON", "\u8BF7\u6C42\u4F53\u4E0D\u662F JSON \u5BF9\u8C61");
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
HttpError,
|
|
48
|
+
hasHeader,
|
|
49
|
+
readJsonBody,
|
|
50
|
+
sendJson
|
|
51
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { D as DetectDeps, I as InstallKind, a as InstallKindResult, P as ProfileLike, c as classifyInstallSpec, d as detectInstallKind, n as normalizeProfile, r as readDeclaredSpec } from './profile-C-eyP5F6.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* semver 解析与比较(只做插件更新需要的部分)。
|
|
5
|
+
*
|
|
6
|
+
* 不引 semver 依赖:更新判定宁可不升级也不要误升,所以非法版本一律返回 undefined,
|
|
7
|
+
* 由调用方按「不更新」处理。支持 `v1.2.3` / `1.2.3-beta.1` / `1.2.3+build`。
|
|
8
|
+
*/
|
|
9
|
+
interface Semver {
|
|
10
|
+
core: [number, number, number];
|
|
11
|
+
prerelease: string[];
|
|
12
|
+
}
|
|
13
|
+
declare function parseSemver(value: unknown): Semver | undefined;
|
|
14
|
+
/** `candidate` 是否严格新于 `current`;任一方不是合法 semver 都返回 false。 */
|
|
15
|
+
declare function isNewerVersion(current: unknown, candidate: unknown): boolean;
|
|
16
|
+
/** 比较两个版本:负数 / 0 / 正数;任一方非法返回 undefined。 */
|
|
17
|
+
declare function compareVersions(left: unknown, right: unknown): number | undefined;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* npm registry 约定:把包名拼成 `/latest` 端点,并按宿主给出的 registry 优先级发请求。
|
|
21
|
+
*
|
|
22
|
+
* 宿主 `pluginManager.registries()` 会把 profile 的镜像 / 私源 / token 计划交出来;
|
|
23
|
+
* 拿不到就退回官方 registry。这里只做纯函数(不含网络),网络在 `latest.ts`。
|
|
24
|
+
*/
|
|
25
|
+
declare const OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
|
|
26
|
+
declare const DEFAULT_TIMEOUT_MS = 8000;
|
|
27
|
+
declare const DEFAULT_CACHE_TTL_MS: number;
|
|
28
|
+
/** 宿主 registry 计划的形状(字段都当可选处理,宿主版本差异不该让我们崩)。 */
|
|
29
|
+
interface RegistriesLike {
|
|
30
|
+
resolved?: unknown;
|
|
31
|
+
registry?: unknown;
|
|
32
|
+
fallbackRegistries?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/** npm 包名转 registry 路径:`@scope/name` → `@scope%2Fname`(保留 @,斜杠编码)。 */
|
|
35
|
+
declare function encodePackageName(name: string): string;
|
|
36
|
+
/** registry 归一成 `https://host/` 形式;非法(非 http(s) / 解析失败)返回 null。 */
|
|
37
|
+
declare function normalizeRegistry(value: unknown): string | null;
|
|
38
|
+
/** 由宿主的 registries() 结果拼出要问的 registry 顺序:resolved → registry → fallback,去重。 */
|
|
39
|
+
declare function registryPlan(registries: unknown): string[];
|
|
40
|
+
/** 手动更新命令:`dsh plugin --profile <name> add <pkg>@latest`。 */
|
|
41
|
+
declare function manualCommand(packageName: string, profileName?: string | null): string;
|
|
42
|
+
|
|
43
|
+
export { DEFAULT_CACHE_TTL_MS, DEFAULT_TIMEOUT_MS, OFFICIAL_REGISTRY, type RegistriesLike, type Semver, compareVersions, encodePackageName, isNewerVersion, manualCommand, normalizeRegistry, parseSemver, registryPlan };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/semver.ts
|
|
2
|
+
var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3
|
+
function parseSemver(value) {
|
|
4
|
+
const text = String(value ?? "").trim();
|
|
5
|
+
const match = SEMVER.exec(text);
|
|
6
|
+
if (match === null) return void 0;
|
|
7
|
+
const major = Number(match[1]);
|
|
8
|
+
const minor = Number(match[2]);
|
|
9
|
+
const patch = Number(match[3]);
|
|
10
|
+
if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || !Number.isSafeInteger(patch)) return void 0;
|
|
11
|
+
return { core: [major, minor, patch], prerelease: match[4] === void 0 ? [] : match[4].split(".") };
|
|
12
|
+
}
|
|
13
|
+
function comparePrerelease(left, right) {
|
|
14
|
+
if (left.length === 0 || right.length === 0) {
|
|
15
|
+
return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
|
|
16
|
+
}
|
|
17
|
+
const length = Math.max(left.length, right.length);
|
|
18
|
+
for (let index = 0; index < length; index += 1) {
|
|
19
|
+
const a = left[index];
|
|
20
|
+
const b = right[index];
|
|
21
|
+
if (a === void 0 || b === void 0) return a === b ? 0 : a === void 0 ? -1 : 1;
|
|
22
|
+
if (a === b) continue;
|
|
23
|
+
const aNumeric = /^\d+$/.test(a);
|
|
24
|
+
const bNumeric = /^\d+$/.test(b);
|
|
25
|
+
if (aNumeric && bNumeric) {
|
|
26
|
+
const an = BigInt(a);
|
|
27
|
+
const bn = BigInt(b);
|
|
28
|
+
if (an !== bn) return an > bn ? 1 : -1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
|
|
32
|
+
return a > b ? 1 : -1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
function isNewerVersion(current, candidate) {
|
|
37
|
+
const a = parseSemver(current);
|
|
38
|
+
const b = parseSemver(candidate);
|
|
39
|
+
if (a === void 0 || b === void 0) return false;
|
|
40
|
+
for (let index = 0; index < 3; index += 1) {
|
|
41
|
+
const left = a.core[index] ?? 0;
|
|
42
|
+
const right = b.core[index] ?? 0;
|
|
43
|
+
if (right !== left) return right > left;
|
|
44
|
+
}
|
|
45
|
+
return comparePrerelease(b.prerelease, a.prerelease) > 0;
|
|
46
|
+
}
|
|
47
|
+
function compareVersions(left, right) {
|
|
48
|
+
const a = parseSemver(left);
|
|
49
|
+
const b = parseSemver(right);
|
|
50
|
+
if (a === void 0 || b === void 0) return void 0;
|
|
51
|
+
for (let index = 0; index < 3; index += 1) {
|
|
52
|
+
const an = a.core[index] ?? 0;
|
|
53
|
+
const bn = b.core[index] ?? 0;
|
|
54
|
+
if (an !== bn) return an > bn ? 1 : -1;
|
|
55
|
+
}
|
|
56
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/registry.ts
|
|
60
|
+
var OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
|
|
61
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
62
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
63
|
+
function encodePackageName(name) {
|
|
64
|
+
const text = name.trim();
|
|
65
|
+
if (text.startsWith("@")) {
|
|
66
|
+
const slash = text.indexOf("/");
|
|
67
|
+
if (slash < 0) return `@${encodeURIComponent(text.slice(1))}`;
|
|
68
|
+
return `@${encodeURIComponent(text.slice(1, slash))}%2F${encodeURIComponent(text.slice(slash + 1))}`;
|
|
69
|
+
}
|
|
70
|
+
return encodeURIComponent(text);
|
|
71
|
+
}
|
|
72
|
+
function normalizeRegistry(value) {
|
|
73
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
74
|
+
const text = value.trim().replace(/\/+$/, "");
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(`${text}/`);
|
|
77
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
78
|
+
return url.href;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function registryPlan(registries) {
|
|
84
|
+
const ordered = [];
|
|
85
|
+
const push = (value) => {
|
|
86
|
+
const normalized = normalizeRegistry(value);
|
|
87
|
+
if (normalized !== null && !ordered.includes(normalized)) ordered.push(normalized);
|
|
88
|
+
};
|
|
89
|
+
if (registries !== null && typeof registries === "object") {
|
|
90
|
+
const plan = registries;
|
|
91
|
+
push(plan.resolved);
|
|
92
|
+
push(plan.registry);
|
|
93
|
+
const fallback = plan.fallbackRegistries;
|
|
94
|
+
if (Array.isArray(fallback)) for (const item of fallback) push(item);
|
|
95
|
+
}
|
|
96
|
+
if (ordered.length === 0) ordered.push(OFFICIAL_REGISTRY);
|
|
97
|
+
return ordered;
|
|
98
|
+
}
|
|
99
|
+
function manualCommand(packageName, profileName) {
|
|
100
|
+
const spec = `${packageName}@latest`;
|
|
101
|
+
return typeof profileName === "string" && profileName !== "" ? `dsh plugin --profile ${profileName} add ${spec}` : `dsh plugin add ${spec}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/profile.ts
|
|
105
|
+
function normalizeProfile(value) {
|
|
106
|
+
if (value === null || typeof value !== "object") return null;
|
|
107
|
+
const raw = value;
|
|
108
|
+
const profile = {};
|
|
109
|
+
if (typeof raw.name === "string" && raw.name !== "") profile.name = raw.name;
|
|
110
|
+
if (typeof raw.dir === "string" && raw.dir !== "") profile.dir = raw.dir;
|
|
111
|
+
if (typeof raw.home === "string" && raw.home !== "") profile.home = raw.home;
|
|
112
|
+
if (raw.packageManager !== null && typeof raw.packageManager === "object") {
|
|
113
|
+
const pm = raw.packageManager;
|
|
114
|
+
const command = typeof pm.command === "string" && pm.command !== "" ? pm.command : void 0;
|
|
115
|
+
const args = Array.isArray(pm.args) ? pm.args.filter((item) => typeof item === "string") : void 0;
|
|
116
|
+
if (command !== void 0 || args !== void 0) {
|
|
117
|
+
profile.packageManager = { ...command === void 0 ? {} : { command }, ...args === void 0 ? {} : { args } };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return profile;
|
|
121
|
+
}
|
|
122
|
+
function classifyInstallSpec(spec) {
|
|
123
|
+
if (typeof spec !== "string") return "unknown";
|
|
124
|
+
const text = spec.trim();
|
|
125
|
+
if (text === "") return "unknown";
|
|
126
|
+
if (text.startsWith("link:")) return "link";
|
|
127
|
+
if (text.startsWith("file:")) return "file";
|
|
128
|
+
if (text.startsWith("workspace:")) return "workspace";
|
|
129
|
+
return "registry";
|
|
130
|
+
}
|
|
131
|
+
async function defaultReadFile(path) {
|
|
132
|
+
const { readFile } = await import("fs/promises");
|
|
133
|
+
return readFile(path, "utf8");
|
|
134
|
+
}
|
|
135
|
+
async function readDeclaredSpec(deps) {
|
|
136
|
+
const { profile, packageName } = deps;
|
|
137
|
+
if (profile === null || typeof profile.dir !== "string" || profile.dir === "") return {};
|
|
138
|
+
const path = `${profile.dir.replace(/\/+$/, "")}/package.json`;
|
|
139
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
140
|
+
const readJson = deps.readJson ?? (async (target) => JSON.parse(await readFile(target)));
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = await readJson(path);
|
|
144
|
+
} catch {
|
|
145
|
+
return { source: "profile-package-json-unreadable" };
|
|
146
|
+
}
|
|
147
|
+
if (parsed === null || typeof parsed !== "object") return { source: "profile-package-json-invalid" };
|
|
148
|
+
const pkg = parsed;
|
|
149
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
150
|
+
const block = pkg[field];
|
|
151
|
+
if (block === null || typeof block !== "object") continue;
|
|
152
|
+
const spec = block[packageName];
|
|
153
|
+
if (typeof spec === "string") return { spec, source: `${path}#${field}` };
|
|
154
|
+
}
|
|
155
|
+
return { source: `${path}#no-entry` };
|
|
156
|
+
}
|
|
157
|
+
async function detectInstallKind(deps) {
|
|
158
|
+
const declared = await readDeclaredSpec(deps);
|
|
159
|
+
const result = { kind: classifyInstallSpec(declared.spec) };
|
|
160
|
+
if (declared.spec !== void 0) result.spec = declared.spec;
|
|
161
|
+
if (declared.source !== void 0) result.source = declared.source;
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
DEFAULT_CACHE_TTL_MS,
|
|
166
|
+
DEFAULT_TIMEOUT_MS,
|
|
167
|
+
OFFICIAL_REGISTRY,
|
|
168
|
+
classifyInstallSpec,
|
|
169
|
+
compareVersions,
|
|
170
|
+
detectInstallKind,
|
|
171
|
+
encodePackageName,
|
|
172
|
+
isNewerVersion,
|
|
173
|
+
manualCommand,
|
|
174
|
+
normalizeProfile,
|
|
175
|
+
normalizeRegistry,
|
|
176
|
+
parseSemver,
|
|
177
|
+
readDeclaredSpec,
|
|
178
|
+
registryPlan
|
|
179
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profile 读取:判断本插件是以什么形态装进当前 profile。
|
|
3
|
+
*
|
|
4
|
+
* 一键更新最危险的场景是「开发用的本地链接」:profile 里写的是 `link:/path/to/repo`,
|
|
5
|
+
* 一旦用 registry 重装,本地的 link 会被换成 npm 版,开发流就被悄悄改掉了。所以
|
|
6
|
+
* 更新前先读 `<profile.dir>/package.json` 的依赖声明,link: / file: / workspace: 一律
|
|
7
|
+
* 关闭一键更新,只给手动命令。
|
|
8
|
+
*/
|
|
9
|
+
type InstallKind = 'registry' | 'link' | 'file' | 'workspace' | 'unknown';
|
|
10
|
+
interface ProfileLike {
|
|
11
|
+
name?: string;
|
|
12
|
+
dir?: string;
|
|
13
|
+
home?: string;
|
|
14
|
+
packageManager?: {
|
|
15
|
+
command?: string;
|
|
16
|
+
args?: string[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface InstallKindResult {
|
|
20
|
+
kind: InstallKind;
|
|
21
|
+
/** profile package.json 里声明的原始 spec(读不到就是 undefined)。 */
|
|
22
|
+
spec?: string;
|
|
23
|
+
/** 说明来源,便于日志与界面提示。 */
|
|
24
|
+
source?: string;
|
|
25
|
+
}
|
|
26
|
+
/** 把宿主 `profileContext`(任意形状)收敛成只含我们关心的字段;拿不到返回 null。 */
|
|
27
|
+
declare function normalizeProfile(value: unknown): ProfileLike | null;
|
|
28
|
+
/** `dependencies` 里声明的 spec 决定安装形态;`npm:` 别名仍按 registry 处理。 */
|
|
29
|
+
declare function classifyInstallSpec(spec: unknown): InstallKind;
|
|
30
|
+
interface DetectDeps {
|
|
31
|
+
profile: ProfileLike | null;
|
|
32
|
+
packageName: string;
|
|
33
|
+
/** 测试注入;默认读真实文件。 */
|
|
34
|
+
readFile?: (path: string) => Promise<string>;
|
|
35
|
+
readJson?: (path: string) => Promise<unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** 读 profile 的 package.json,返回本插件声明的 spec(找不到返回 undefined)。 */
|
|
38
|
+
declare function readDeclaredSpec(deps: DetectDeps): Promise<{
|
|
39
|
+
spec?: string;
|
|
40
|
+
source?: string;
|
|
41
|
+
}>;
|
|
42
|
+
declare function detectInstallKind(deps: DetectDeps): Promise<InstallKindResult>;
|
|
43
|
+
|
|
44
|
+
export { type DetectDeps as D, type InstallKind as I, type ProfileLike as P, type InstallKindResult as a, classifyInstallSpec as c, detectInstallKind as d, normalizeProfile as n, readDeclaredSpec as r };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { UpdateView, ApplyResult } from './update.js';
|
|
2
|
+
import './profile-C-eyP5F6.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 客户端侧 headless 控制器(不依赖 React、不 import node 内置模块)。
|
|
6
|
+
*
|
|
7
|
+
* 各插件的 UI 长得不一样,共享「状态机 + 单飞 + 错误收敛」就够了:
|
|
8
|
+
* 插件用自己的 React / h() 订阅 `subscribe()`,按 `status` 渲染按钮与进度,
|
|
9
|
+
* 文案与样式留在各插件里(错误码由插件翻译,不在这里塞 i18n)。
|
|
10
|
+
*
|
|
11
|
+
* 状态:idle → checking → available | current | failed
|
|
12
|
+
* available → applying → applied | restart-required | failed
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type UpdateClientStatus = 'idle' | 'checking' | 'available' | 'current' | 'failed' | 'applying' | 'applied' | 'restart-required';
|
|
16
|
+
/** 只保留界面要用的字段,便于测试与跨版本兼容。 */
|
|
17
|
+
type UpdateClientView = Pick<UpdateView, 'packageName' | 'currentVersion' | 'latestVersion' | 'updateAvailable' | 'checkFailed' | 'canApply' | 'installKind' | 'manualCommand'> & {
|
|
18
|
+
applying?: boolean;
|
|
19
|
+
installSpec?: string;
|
|
20
|
+
};
|
|
21
|
+
interface UpdateClientError {
|
|
22
|
+
code?: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
interface UpdateClientState {
|
|
26
|
+
status: UpdateClientStatus;
|
|
27
|
+
view: UpdateClientView | null;
|
|
28
|
+
error: UpdateClientError | null;
|
|
29
|
+
/** 最近一次检查完成时间(ISO);没检查过就是 undefined。 */
|
|
30
|
+
checkedAt?: string;
|
|
31
|
+
}
|
|
32
|
+
interface UpdateRequest {
|
|
33
|
+
check(options: {
|
|
34
|
+
fresh?: boolean;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}): Promise<UpdateClientView>;
|
|
37
|
+
apply(): Promise<Pick<ApplyResult, 'application' | 'updatedVersion'> & {
|
|
38
|
+
restartRequired?: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
interface UpdateControllerOptions {
|
|
42
|
+
request: UpdateRequest;
|
|
43
|
+
onChange?: (state: UpdateClientState) => void;
|
|
44
|
+
now?: () => number;
|
|
45
|
+
}
|
|
46
|
+
interface UpdateController {
|
|
47
|
+
getState(): UpdateClientState;
|
|
48
|
+
subscribe(listener: (state: UpdateClientState) => void): () => void;
|
|
49
|
+
check(options?: {
|
|
50
|
+
fresh?: boolean;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
apply(): Promise<void>;
|
|
53
|
+
reset(): void;
|
|
54
|
+
}
|
|
55
|
+
declare function createUpdateController(options: UpdateControllerOptions): UpdateController;
|
|
56
|
+
|
|
57
|
+
export { type UpdateClientError, type UpdateClientState, type UpdateClientStatus, type UpdateClientView, type UpdateController, type UpdateControllerOptions, type UpdateRequest, createUpdateController };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/update-client.ts
|
|
2
|
+
var IDLE = { status: "idle", view: null, error: null };
|
|
3
|
+
function errorOf(error) {
|
|
4
|
+
if (error !== null && typeof error === "object") {
|
|
5
|
+
const value = error;
|
|
6
|
+
const message = typeof value.message === "string" && value.message !== "" ? value.message : "\u64CD\u4F5C\u5931\u8D25";
|
|
7
|
+
return typeof value.code === "string" ? { code: value.code, message } : { message };
|
|
8
|
+
}
|
|
9
|
+
return { message: String(error ?? "\u64CD\u4F5C\u5931\u8D25") };
|
|
10
|
+
}
|
|
11
|
+
function createUpdateController(options) {
|
|
12
|
+
const now = options.now ?? Date.now;
|
|
13
|
+
let state = { ...IDLE };
|
|
14
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
let inFlight = null;
|
|
16
|
+
function emit(next) {
|
|
17
|
+
state = next;
|
|
18
|
+
options.onChange?.(state);
|
|
19
|
+
for (const listener of listeners) listener(state);
|
|
20
|
+
}
|
|
21
|
+
async function check(checkOptions = {}) {
|
|
22
|
+
if (inFlight !== null) return;
|
|
23
|
+
inFlight = "check";
|
|
24
|
+
emit({ ...state, status: "checking", error: null });
|
|
25
|
+
try {
|
|
26
|
+
const view = await options.request.check(checkOptions);
|
|
27
|
+
const checkedAt = new Date(now()).toISOString();
|
|
28
|
+
if (view.checkFailed) {
|
|
29
|
+
emit({
|
|
30
|
+
status: "failed",
|
|
31
|
+
view,
|
|
32
|
+
error: { code: "CHECK_FAILED", message: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF1A\u62FF\u4E0D\u5230 registry \u4E0A\u7684\u6700\u65B0\u7248\u672C" },
|
|
33
|
+
checkedAt
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
emit({ status: view.updateAvailable ? "available" : "current", view, error: null, checkedAt });
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
emit({ ...state, status: "failed", error: errorOf(error) });
|
|
40
|
+
} finally {
|
|
41
|
+
inFlight = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function apply() {
|
|
45
|
+
if (inFlight !== null) return;
|
|
46
|
+
const view = state.view;
|
|
47
|
+
if (view === null || !view.updateAvailable) {
|
|
48
|
+
emit({ ...state, status: "failed", error: { code: "NO_UPDATE", message: "\u6CA1\u6709\u53EF\u66F4\u65B0\u7684\u7248\u672C\uFF0C\u8BF7\u5148\u68C0\u67E5\u66F4\u65B0" } });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!view.canApply) {
|
|
52
|
+
emit({
|
|
53
|
+
...state,
|
|
54
|
+
status: "failed",
|
|
55
|
+
error: { code: "NO_APPLY", message: `\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u4E00\u952E\u66F4\u65B0\uFF0C\u8BF7\u6267\u884C\uFF1A${view.manualCommand}` }
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
inFlight = "apply";
|
|
60
|
+
emit({ ...state, status: "applying", error: null });
|
|
61
|
+
try {
|
|
62
|
+
const result = await options.request.apply();
|
|
63
|
+
const restartRequired = result.restartRequired ?? result.application !== "applied";
|
|
64
|
+
emit({ ...state, status: restartRequired ? "restart-required" : "applied", error: null });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
emit({ ...state, status: "failed", error: errorOf(error) });
|
|
67
|
+
} finally {
|
|
68
|
+
inFlight = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
getState: () => state,
|
|
73
|
+
subscribe: (listener) => {
|
|
74
|
+
listeners.add(listener);
|
|
75
|
+
return () => {
|
|
76
|
+
listeners.delete(listener);
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
check,
|
|
80
|
+
apply,
|
|
81
|
+
reset: () => emit({ ...IDLE })
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
createUpdateController
|
|
86
|
+
};
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { I as InstallKind } from './profile-C-eyP5F6.js';
|
|
2
|
+
export { P as ProfileLike } from './profile-C-eyP5F6.js';
|
|
3
|
+
|
|
4
|
+
type Logger = (event: string, detail?: Record<string, unknown>) => void;
|
|
5
|
+
interface PluginMeta {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
repository?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 插件自更新的 host 引擎:查最新版 → 判定能否一键更新 → 经宿主 `pluginManager` 重装。
|
|
13
|
+
*
|
|
14
|
+
* 设计约束(三个 DSH 插件共用这份逻辑):
|
|
15
|
+
* - **零 `@deepseek-ai/*` 运行时依赖**:pluginManager / profileContext 都由调用方用
|
|
16
|
+
* `ctx.get(...)` 惰性取,拿不到就退化成「只检查 + 手动命令」。
|
|
17
|
+
* - **不自作主张升级**:只有 registry 明确给出合法且严格更新的版本才允许安装;
|
|
18
|
+
* `link:` / `file:` / `workspace:` 安装一律禁止一键更新(会把开发链接换成 npm 版)。
|
|
19
|
+
* - **装不上要能回退**:宿主 `installBundle` 对已装包会返回 `already-installed`,按官方
|
|
20
|
+
* 更新路径先 remove 再 install;remove 之后 install 失败则尝试装回原版本。
|
|
21
|
+
* - **不重写宿主进程策略**:需要 `pnpm view` 时由调用方注入 `runPnpmView`(进程/环境
|
|
22
|
+
* 白名单是各插件自己的事),本包只做 registry 兜底。
|
|
23
|
+
* - 更新会把运行中的插件自己卸载,界面会一起消失:全过程落固定事件名,便于事后对账。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
type UpdateErrorCode = 'NO_MANAGER' | 'LINKED_INSTALL' | 'NOT_CHECKED' | 'NO_UPDATE' | 'INVALID_VERSION' | 'APPLY_IN_PROGRESS' | 'INSTALL_FAILED' | 'REMOVE_FAILED' | 'ROLLBACK_FAILED' | 'UNSUPPORTED_RESULT';
|
|
27
|
+
declare class PluginUpdateError extends Error {
|
|
28
|
+
readonly code: UpdateErrorCode;
|
|
29
|
+
readonly detail: Record<string, unknown>;
|
|
30
|
+
constructor(code: UpdateErrorCode, message: string, detail?: Record<string, unknown>);
|
|
31
|
+
}
|
|
32
|
+
interface UpdateView {
|
|
33
|
+
packageName: string;
|
|
34
|
+
currentVersion: string;
|
|
35
|
+
latestVersion: string | null;
|
|
36
|
+
updateAvailable: boolean;
|
|
37
|
+
/** 查询失败(网络 / registry 都不可用),不等于「已是最新」。 */
|
|
38
|
+
checkFailed: boolean;
|
|
39
|
+
canApply: boolean;
|
|
40
|
+
installKind: InstallKind;
|
|
41
|
+
installSpec?: string;
|
|
42
|
+
manualCommand: string;
|
|
43
|
+
applying: boolean;
|
|
44
|
+
checkedAt?: string;
|
|
45
|
+
}
|
|
46
|
+
interface ApplyResult {
|
|
47
|
+
application: 'applied' | 'restart-required' | 'overridden';
|
|
48
|
+
restartRequired: boolean;
|
|
49
|
+
updatedVersion: string;
|
|
50
|
+
installKind: InstallKind;
|
|
51
|
+
/** 实际走过的步骤,给界面/日志做进度展示。 */
|
|
52
|
+
steps: string[];
|
|
53
|
+
}
|
|
54
|
+
interface PluginManagerLike {
|
|
55
|
+
installBundle?: (spec: string) => Promise<unknown>;
|
|
56
|
+
removeBundle?: (name: string) => Promise<unknown>;
|
|
57
|
+
registries?: () => unknown;
|
|
58
|
+
}
|
|
59
|
+
interface UpdaterDeps {
|
|
60
|
+
meta: PluginMeta;
|
|
61
|
+
getPluginManager?: () => unknown;
|
|
62
|
+
getProfile?: () => unknown;
|
|
63
|
+
getProfileName?: () => string | null;
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
runPnpmView?: (spec: string) => Promise<string | null>;
|
|
66
|
+
readFile?: (path: string) => Promise<string>;
|
|
67
|
+
logger?: Logger;
|
|
68
|
+
now?: () => number;
|
|
69
|
+
cacheTtlMs?: number;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
}
|
|
72
|
+
interface Updater {
|
|
73
|
+
check(options?: {
|
|
74
|
+
fresh?: boolean;
|
|
75
|
+
}): Promise<UpdateView>;
|
|
76
|
+
view(): UpdateView | null;
|
|
77
|
+
latestVersion(options?: {
|
|
78
|
+
fresh?: boolean;
|
|
79
|
+
}): Promise<string | null>;
|
|
80
|
+
apply(options?: {
|
|
81
|
+
version?: string;
|
|
82
|
+
}): Promise<ApplyResult>;
|
|
83
|
+
isApplying(): boolean;
|
|
84
|
+
}
|
|
85
|
+
declare function createPluginUpdater(deps: UpdaterDeps): Updater;
|
|
86
|
+
|
|
87
|
+
export { type ApplyResult, InstallKind, type Logger, type PluginManagerLike, type PluginMeta, PluginUpdateError, type UpdateErrorCode, type UpdateView, type Updater, type UpdaterDeps, createPluginUpdater };
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// src/registry.ts
|
|
2
|
+
var OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
|
|
3
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
4
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
5
|
+
function encodePackageName(name) {
|
|
6
|
+
const text = name.trim();
|
|
7
|
+
if (text.startsWith("@")) {
|
|
8
|
+
const slash = text.indexOf("/");
|
|
9
|
+
if (slash < 0) return `@${encodeURIComponent(text.slice(1))}`;
|
|
10
|
+
return `@${encodeURIComponent(text.slice(1, slash))}%2F${encodeURIComponent(text.slice(slash + 1))}`;
|
|
11
|
+
}
|
|
12
|
+
return encodeURIComponent(text);
|
|
13
|
+
}
|
|
14
|
+
function normalizeRegistry(value) {
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
16
|
+
const text = value.trim().replace(/\/+$/, "");
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(`${text}/`);
|
|
19
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
20
|
+
return url.href;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function registryPlan(registries) {
|
|
26
|
+
const ordered = [];
|
|
27
|
+
const push = (value) => {
|
|
28
|
+
const normalized = normalizeRegistry(value);
|
|
29
|
+
if (normalized !== null && !ordered.includes(normalized)) ordered.push(normalized);
|
|
30
|
+
};
|
|
31
|
+
if (registries !== null && typeof registries === "object") {
|
|
32
|
+
const plan = registries;
|
|
33
|
+
push(plan.resolved);
|
|
34
|
+
push(plan.registry);
|
|
35
|
+
const fallback = plan.fallbackRegistries;
|
|
36
|
+
if (Array.isArray(fallback)) for (const item of fallback) push(item);
|
|
37
|
+
}
|
|
38
|
+
if (ordered.length === 0) ordered.push(OFFICIAL_REGISTRY);
|
|
39
|
+
return ordered;
|
|
40
|
+
}
|
|
41
|
+
function manualCommand(packageName, profileName) {
|
|
42
|
+
const spec = `${packageName}@latest`;
|
|
43
|
+
return typeof profileName === "string" && profileName !== "" ? `dsh plugin --profile ${profileName} add ${spec}` : `dsh plugin add ${spec}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/semver.ts
|
|
47
|
+
var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
48
|
+
function parseSemver(value) {
|
|
49
|
+
const text = String(value ?? "").trim();
|
|
50
|
+
const match = SEMVER.exec(text);
|
|
51
|
+
if (match === null) return void 0;
|
|
52
|
+
const major = Number(match[1]);
|
|
53
|
+
const minor = Number(match[2]);
|
|
54
|
+
const patch = Number(match[3]);
|
|
55
|
+
if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || !Number.isSafeInteger(patch)) return void 0;
|
|
56
|
+
return { core: [major, minor, patch], prerelease: match[4] === void 0 ? [] : match[4].split(".") };
|
|
57
|
+
}
|
|
58
|
+
function comparePrerelease(left, right) {
|
|
59
|
+
if (left.length === 0 || right.length === 0) {
|
|
60
|
+
return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
|
|
61
|
+
}
|
|
62
|
+
const length = Math.max(left.length, right.length);
|
|
63
|
+
for (let index = 0; index < length; index += 1) {
|
|
64
|
+
const a = left[index];
|
|
65
|
+
const b = right[index];
|
|
66
|
+
if (a === void 0 || b === void 0) return a === b ? 0 : a === void 0 ? -1 : 1;
|
|
67
|
+
if (a === b) continue;
|
|
68
|
+
const aNumeric = /^\d+$/.test(a);
|
|
69
|
+
const bNumeric = /^\d+$/.test(b);
|
|
70
|
+
if (aNumeric && bNumeric) {
|
|
71
|
+
const an = BigInt(a);
|
|
72
|
+
const bn = BigInt(b);
|
|
73
|
+
if (an !== bn) return an > bn ? 1 : -1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
|
|
77
|
+
return a > b ? 1 : -1;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
function isNewerVersion(current, candidate) {
|
|
82
|
+
const a = parseSemver(current);
|
|
83
|
+
const b = parseSemver(candidate);
|
|
84
|
+
if (a === void 0 || b === void 0) return false;
|
|
85
|
+
for (let index = 0; index < 3; index += 1) {
|
|
86
|
+
const left = a.core[index] ?? 0;
|
|
87
|
+
const right = b.core[index] ?? 0;
|
|
88
|
+
if (right !== left) return right > left;
|
|
89
|
+
}
|
|
90
|
+
return comparePrerelease(b.prerelease, a.prerelease) > 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/latest.ts
|
|
94
|
+
function createLatestResolver(deps) {
|
|
95
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
96
|
+
const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
97
|
+
const now = deps.now ?? Date.now;
|
|
98
|
+
const logger = deps.logger ?? (() => {
|
|
99
|
+
});
|
|
100
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
101
|
+
let cache = null;
|
|
102
|
+
function manager() {
|
|
103
|
+
try {
|
|
104
|
+
const value = deps.getPluginManager?.();
|
|
105
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function readRegistries() {
|
|
111
|
+
const service = manager();
|
|
112
|
+
if (service === null || typeof service.registries !== "function") return null;
|
|
113
|
+
try {
|
|
114
|
+
return await service.registries();
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function remember(version, source) {
|
|
120
|
+
cache = { version, expiresAt: now() + cacheTtlMs, source };
|
|
121
|
+
return version;
|
|
122
|
+
}
|
|
123
|
+
async function viaPnpm() {
|
|
124
|
+
if (typeof deps.runPnpmView !== "function") return null;
|
|
125
|
+
try {
|
|
126
|
+
const value = await deps.runPnpmView(`${deps.meta.name}@latest`);
|
|
127
|
+
const version = typeof value === "string" ? value.trim() : "";
|
|
128
|
+
return parseSemver(version) === void 0 ? null : version;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function viaRegistries() {
|
|
134
|
+
const plan = registryPlan(await readRegistries());
|
|
135
|
+
for (const base of plan) {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetchImpl(`${base}${encodePackageName(deps.meta.name)}/latest`, {
|
|
138
|
+
headers: { accept: "application/json" },
|
|
139
|
+
redirect: "error",
|
|
140
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) continue;
|
|
143
|
+
const value = await response.json();
|
|
144
|
+
const version = typeof value.version === "string" ? value.version.trim() : "";
|
|
145
|
+
if (parseSemver(version) !== void 0) return { version, source: base };
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
async function latest(options = {}) {
|
|
152
|
+
if (deps.meta.name.trim() === "") return null;
|
|
153
|
+
if (options.fresh === true) cache = null;
|
|
154
|
+
if (cache !== null && now() < cache.expiresAt) return cache.version;
|
|
155
|
+
const pnpm = await viaPnpm();
|
|
156
|
+
if (pnpm !== null) {
|
|
157
|
+
logger("plugin.update.latest", { source: "pnpm", version: pnpm });
|
|
158
|
+
return remember(pnpm, "pnpm");
|
|
159
|
+
}
|
|
160
|
+
const registry = await viaRegistries();
|
|
161
|
+
if (registry !== null) {
|
|
162
|
+
logger("plugin.update.latest", { source: registry.source, version: registry.version });
|
|
163
|
+
return remember(registry.version, registry.source);
|
|
164
|
+
}
|
|
165
|
+
logger("plugin.update.latest.failed", { package: deps.meta.name });
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return { latest, peek: () => cache === null ? null : { ...cache } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/profile.ts
|
|
172
|
+
function normalizeProfile(value) {
|
|
173
|
+
if (value === null || typeof value !== "object") return null;
|
|
174
|
+
const raw = value;
|
|
175
|
+
const profile = {};
|
|
176
|
+
if (typeof raw.name === "string" && raw.name !== "") profile.name = raw.name;
|
|
177
|
+
if (typeof raw.dir === "string" && raw.dir !== "") profile.dir = raw.dir;
|
|
178
|
+
if (typeof raw.home === "string" && raw.home !== "") profile.home = raw.home;
|
|
179
|
+
if (raw.packageManager !== null && typeof raw.packageManager === "object") {
|
|
180
|
+
const pm = raw.packageManager;
|
|
181
|
+
const command = typeof pm.command === "string" && pm.command !== "" ? pm.command : void 0;
|
|
182
|
+
const args = Array.isArray(pm.args) ? pm.args.filter((item) => typeof item === "string") : void 0;
|
|
183
|
+
if (command !== void 0 || args !== void 0) {
|
|
184
|
+
profile.packageManager = { ...command === void 0 ? {} : { command }, ...args === void 0 ? {} : { args } };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return profile;
|
|
188
|
+
}
|
|
189
|
+
function classifyInstallSpec(spec) {
|
|
190
|
+
if (typeof spec !== "string") return "unknown";
|
|
191
|
+
const text = spec.trim();
|
|
192
|
+
if (text === "") return "unknown";
|
|
193
|
+
if (text.startsWith("link:")) return "link";
|
|
194
|
+
if (text.startsWith("file:")) return "file";
|
|
195
|
+
if (text.startsWith("workspace:")) return "workspace";
|
|
196
|
+
return "registry";
|
|
197
|
+
}
|
|
198
|
+
async function defaultReadFile(path) {
|
|
199
|
+
const { readFile } = await import("fs/promises");
|
|
200
|
+
return readFile(path, "utf8");
|
|
201
|
+
}
|
|
202
|
+
async function readDeclaredSpec(deps) {
|
|
203
|
+
const { profile, packageName } = deps;
|
|
204
|
+
if (profile === null || typeof profile.dir !== "string" || profile.dir === "") return {};
|
|
205
|
+
const path = `${profile.dir.replace(/\/+$/, "")}/package.json`;
|
|
206
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
207
|
+
const readJson = deps.readJson ?? (async (target) => JSON.parse(await readFile(target)));
|
|
208
|
+
let parsed;
|
|
209
|
+
try {
|
|
210
|
+
parsed = await readJson(path);
|
|
211
|
+
} catch {
|
|
212
|
+
return { source: "profile-package-json-unreadable" };
|
|
213
|
+
}
|
|
214
|
+
if (parsed === null || typeof parsed !== "object") return { source: "profile-package-json-invalid" };
|
|
215
|
+
const pkg = parsed;
|
|
216
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
217
|
+
const block = pkg[field];
|
|
218
|
+
if (block === null || typeof block !== "object") continue;
|
|
219
|
+
const spec = block[packageName];
|
|
220
|
+
if (typeof spec === "string") return { spec, source: `${path}#${field}` };
|
|
221
|
+
}
|
|
222
|
+
return { source: `${path}#no-entry` };
|
|
223
|
+
}
|
|
224
|
+
async function detectInstallKind(deps) {
|
|
225
|
+
const declared = await readDeclaredSpec(deps);
|
|
226
|
+
const result = { kind: classifyInstallSpec(declared.spec) };
|
|
227
|
+
if (declared.spec !== void 0) result.spec = declared.spec;
|
|
228
|
+
if (declared.source !== void 0) result.source = declared.source;
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/update.ts
|
|
233
|
+
var PluginUpdateError = class extends Error {
|
|
234
|
+
code;
|
|
235
|
+
detail;
|
|
236
|
+
constructor(code, message, detail = {}) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.name = "PluginUpdateError";
|
|
239
|
+
this.code = code;
|
|
240
|
+
this.detail = detail;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
function unwrapResult(result) {
|
|
244
|
+
if (result === null || typeof result !== "object") return {};
|
|
245
|
+
const value = result;
|
|
246
|
+
if (typeof value.ok === "boolean" && !("application" in value) && ("value" in value || "error" in value)) {
|
|
247
|
+
return value.ok ? unwrapResult(value.value) : { error: asError(value.error) };
|
|
248
|
+
}
|
|
249
|
+
const unwrapped = {};
|
|
250
|
+
if ("application" in value) unwrapped.application = value.application;
|
|
251
|
+
if ("error" in value && value.error !== void 0 && value.error !== null) unwrapped.error = asError(value.error);
|
|
252
|
+
return unwrapped;
|
|
253
|
+
}
|
|
254
|
+
function asError(value) {
|
|
255
|
+
if (value !== null && typeof value === "object") return value;
|
|
256
|
+
return { message: String(value ?? "") };
|
|
257
|
+
}
|
|
258
|
+
function failureMessage(error) {
|
|
259
|
+
if (error === void 0) return "";
|
|
260
|
+
for (const key of ["diagnostic", "message", "code"]) {
|
|
261
|
+
const value = error[key];
|
|
262
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
263
|
+
}
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
var APPLICATIONS = /* @__PURE__ */ new Set(["applied", "restart-required", "overridden"]);
|
|
267
|
+
function createPluginUpdater(deps) {
|
|
268
|
+
const logger = deps.logger ?? (() => {
|
|
269
|
+
});
|
|
270
|
+
const now = deps.now ?? Date.now;
|
|
271
|
+
const resolver = createLatestResolver({
|
|
272
|
+
meta: deps.meta,
|
|
273
|
+
...deps.getPluginManager === void 0 ? {} : { getPluginManager: deps.getPluginManager },
|
|
274
|
+
...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
|
|
275
|
+
...deps.runPnpmView === void 0 ? {} : { runPnpmView: deps.runPnpmView },
|
|
276
|
+
...deps.now === void 0 ? {} : { now: deps.now },
|
|
277
|
+
...deps.cacheTtlMs === void 0 ? {} : { cacheTtlMs: deps.cacheTtlMs },
|
|
278
|
+
...deps.timeoutMs === void 0 ? {} : { timeoutMs: deps.timeoutMs },
|
|
279
|
+
logger
|
|
280
|
+
});
|
|
281
|
+
let lastView = null;
|
|
282
|
+
let applying = false;
|
|
283
|
+
function manager() {
|
|
284
|
+
try {
|
|
285
|
+
const value = deps.getPluginManager?.();
|
|
286
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function profile() {
|
|
292
|
+
try {
|
|
293
|
+
return normalizeProfile(deps.getProfile?.());
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function profileName() {
|
|
299
|
+
try {
|
|
300
|
+
const fromDep = deps.getProfileName?.();
|
|
301
|
+
if (typeof fromDep === "string" && fromDep !== "") return fromDep;
|
|
302
|
+
return profile()?.name ?? null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function canApplyWith(kind) {
|
|
308
|
+
const service = manager();
|
|
309
|
+
if (service === null || typeof service.installBundle !== "function") return false;
|
|
310
|
+
return kind === "registry" || kind === "unknown";
|
|
311
|
+
}
|
|
312
|
+
async function check(options = {}) {
|
|
313
|
+
const install = await detectInstallKind({
|
|
314
|
+
profile: profile(),
|
|
315
|
+
packageName: deps.meta.name,
|
|
316
|
+
...deps.readFile === void 0 ? {} : { readFile: deps.readFile }
|
|
317
|
+
});
|
|
318
|
+
const latest = await resolver.latest(options.fresh === true ? { fresh: true } : {});
|
|
319
|
+
const view = {
|
|
320
|
+
packageName: deps.meta.name,
|
|
321
|
+
currentVersion: deps.meta.version,
|
|
322
|
+
latestVersion: latest,
|
|
323
|
+
updateAvailable: latest !== null && isNewerVersion(deps.meta.version, latest),
|
|
324
|
+
checkFailed: latest === null,
|
|
325
|
+
canApply: canApplyWith(install.kind),
|
|
326
|
+
installKind: install.kind,
|
|
327
|
+
...install.spec === void 0 ? {} : { installSpec: install.spec },
|
|
328
|
+
manualCommand: manualCommand(deps.meta.name, profileName()),
|
|
329
|
+
applying,
|
|
330
|
+
checkedAt: new Date(now()).toISOString()
|
|
331
|
+
};
|
|
332
|
+
lastView = view;
|
|
333
|
+
logger("plugin.update.check", {
|
|
334
|
+
current: view.currentVersion,
|
|
335
|
+
latest: view.latestVersion,
|
|
336
|
+
updateAvailable: view.updateAvailable,
|
|
337
|
+
checkFailed: view.checkFailed,
|
|
338
|
+
canApply: view.canApply,
|
|
339
|
+
installKind: view.installKind
|
|
340
|
+
});
|
|
341
|
+
return view;
|
|
342
|
+
}
|
|
343
|
+
async function apply(options = {}) {
|
|
344
|
+
if (applying) throw new PluginUpdateError("APPLY_IN_PROGRESS", "\u5DF2\u6709\u4E00\u6B21\u66F4\u65B0\u5728\u8FDB\u884C\u4E2D");
|
|
345
|
+
const view = lastView;
|
|
346
|
+
if (view === null) {
|
|
347
|
+
throw new PluginUpdateError("NOT_CHECKED", "\u8FD8\u6CA1\u6709\u68C0\u67E5\u8FC7\u66F4\u65B0\uFF0C\u8BF7\u5148\u68C0\u67E5\u518D\u66F4\u65B0");
|
|
348
|
+
}
|
|
349
|
+
if (view.checkFailed || view.latestVersion === null) {
|
|
350
|
+
throw new PluginUpdateError("NOT_CHECKED", "\u8FD8\u6CA1\u6709\u62FF\u5230\u53EF\u7528\u7684\u6700\u65B0\u7248\u672C\uFF0C\u8BF7\u5148\u68C0\u67E5\u66F4\u65B0", { checkFailed: view.checkFailed });
|
|
351
|
+
}
|
|
352
|
+
const target = view.latestVersion;
|
|
353
|
+
if (options.version !== void 0 && options.version !== target) {
|
|
354
|
+
throw new PluginUpdateError("INVALID_VERSION", `\u53EA\u80FD\u66F4\u65B0\u5230\u521A\u67E5\u5230\u7684 ${target}\uFF0C\u4E0D\u63A5\u53D7 ${options.version}`, {
|
|
355
|
+
requested: options.version,
|
|
356
|
+
target
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (!view.updateAvailable || !isNewerVersion(deps.meta.version, target)) {
|
|
360
|
+
throw new PluginUpdateError("NO_UPDATE", `\u5F53\u524D v${deps.meta.version} \u5DF2\u662F\u6700\u65B0\uFF08registry: ${target}\uFF09`);
|
|
361
|
+
}
|
|
362
|
+
if (parseSemver(target) === void 0) {
|
|
363
|
+
throw new PluginUpdateError("INVALID_VERSION", `registry \u8FD4\u56DE\u7684\u7248\u672C\u4E0D\u662F\u5408\u6CD5 semver\uFF1A${target}`);
|
|
364
|
+
}
|
|
365
|
+
const install = await detectInstallKind({
|
|
366
|
+
profile: profile(),
|
|
367
|
+
packageName: deps.meta.name,
|
|
368
|
+
...deps.readFile === void 0 ? {} : { readFile: deps.readFile }
|
|
369
|
+
});
|
|
370
|
+
if (install.kind === "link" || install.kind === "file" || install.kind === "workspace") {
|
|
371
|
+
throw new PluginUpdateError(
|
|
372
|
+
"LINKED_INSTALL",
|
|
373
|
+
`\u5F53\u524D\u662F\u672C\u5730 ${install.kind} \u5B89\u88C5\uFF08${install.spec ?? "\u672A\u77E5 spec"}\uFF09\uFF0C\u4E0D\u80FD\u7528 registry \u7248\u672C\u8986\u76D6\uFF1B\u8BF7\u624B\u52A8\u6267\u884C ${view.manualCommand}`,
|
|
374
|
+
{ installKind: install.kind, spec: install.spec }
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const service = manager();
|
|
378
|
+
if (service === null || typeof service.installBundle !== "function") {
|
|
379
|
+
throw new PluginUpdateError("NO_MANAGER", "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u4E00\u952E\u66F4\u65B0\uFF08\u7F3A\u5C11 pluginManager.installBundle\uFF09", {
|
|
380
|
+
manualCommand: view.manualCommand
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const installBundle = service.installBundle.bind(service);
|
|
384
|
+
const removeBundle = typeof service.removeBundle === "function" ? service.removeBundle.bind(service) : null;
|
|
385
|
+
const spec = `${deps.meta.name}@${target}`;
|
|
386
|
+
const steps = [];
|
|
387
|
+
applying = true;
|
|
388
|
+
lastView = { ...view, applying: true };
|
|
389
|
+
logger("plugin.update.apply.start", { from: deps.meta.version, target, installKind: install.kind });
|
|
390
|
+
try {
|
|
391
|
+
steps.push(`install ${spec}`);
|
|
392
|
+
let installed = unwrapResult(await installBundle(spec));
|
|
393
|
+
let removed = false;
|
|
394
|
+
if (installed.error?.code === "already-installed" && removeBundle !== null) {
|
|
395
|
+
steps.push(`remove ${deps.meta.name}`);
|
|
396
|
+
const removal = unwrapResult(await removeBundle(deps.meta.name));
|
|
397
|
+
if (removal.error !== void 0 || removal.application === "failed") {
|
|
398
|
+
throw new PluginUpdateError("REMOVE_FAILED", `\u79FB\u9664\u65E7\u7248\u672C\u5931\u8D25\uFF1A${failureMessage(removal.error)}`, { steps });
|
|
399
|
+
}
|
|
400
|
+
removed = true;
|
|
401
|
+
steps.push(`install ${spec}`);
|
|
402
|
+
installed = unwrapResult(await installBundle(spec));
|
|
403
|
+
}
|
|
404
|
+
if (installed.error !== void 0) {
|
|
405
|
+
const message = failureMessage(installed.error) || "\u5B89\u88C5\u65B0\u7248\u672C\u5931\u8D25";
|
|
406
|
+
if (removed) {
|
|
407
|
+
steps.push(`rollback ${deps.meta.name}@${deps.meta.version}`);
|
|
408
|
+
let rollback;
|
|
409
|
+
try {
|
|
410
|
+
rollback = unwrapResult(await installBundle(`${deps.meta.name}@${deps.meta.version}`));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
throw new PluginUpdateError("ROLLBACK_FAILED", `\u66F4\u65B0\u5931\u8D25\u4E14\u56DE\u6EDA\u5931\u8D25\uFF1A${message}`, {
|
|
413
|
+
steps,
|
|
414
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (rollback.error !== void 0) {
|
|
418
|
+
throw new PluginUpdateError("ROLLBACK_FAILED", `\u66F4\u65B0\u5931\u8D25\u4E14\u56DE\u6EDA\u5931\u8D25\uFF1A${message}`, {
|
|
419
|
+
steps,
|
|
420
|
+
rollback: failureMessage(rollback.error)
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
throw new PluginUpdateError("INSTALL_FAILED", `\u5B89\u88C5 v${target} \u5931\u8D25\uFF1A${message}`, { steps });
|
|
425
|
+
}
|
|
426
|
+
const application = typeof installed.application === "string" ? installed.application : "unknown";
|
|
427
|
+
if (!APPLICATIONS.has(application)) {
|
|
428
|
+
throw new PluginUpdateError("UNSUPPORTED_RESULT", `\u66F4\u65B0\u672A\u751F\u6548\uFF08application=${application}\uFF09`, { steps });
|
|
429
|
+
}
|
|
430
|
+
const result = {
|
|
431
|
+
application,
|
|
432
|
+
restartRequired: application !== "applied",
|
|
433
|
+
updatedVersion: target,
|
|
434
|
+
installKind: install.kind,
|
|
435
|
+
steps
|
|
436
|
+
};
|
|
437
|
+
logger("plugin.update.apply", { ...result });
|
|
438
|
+
return result;
|
|
439
|
+
} catch (error) {
|
|
440
|
+
const normalized = error instanceof PluginUpdateError ? error : new PluginUpdateError("INSTALL_FAILED", error instanceof Error ? error.message : String(error), { steps });
|
|
441
|
+
logger("plugin.update.apply.failed", { code: normalized.code, message: normalized.message, steps });
|
|
442
|
+
throw normalized;
|
|
443
|
+
} finally {
|
|
444
|
+
applying = false;
|
|
445
|
+
if (lastView !== null) lastView = { ...lastView, applying: false };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
check,
|
|
450
|
+
view: () => lastView,
|
|
451
|
+
latestVersion: (options = {}) => resolver.latest(options),
|
|
452
|
+
apply,
|
|
453
|
+
isApplying: () => applying
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
export {
|
|
457
|
+
PluginUpdateError,
|
|
458
|
+
createPluginUpdater
|
|
459
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morehao/dsh-plugin-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness 插件共用工具包:插件自更新引擎(查 npm 最新版 + 经宿主 pluginManager 重装)、headless 客户端控制器与 HTTP 小工具。零 @deepseek-ai 运行时依赖,宿主能力全部注入。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./update": {
|
|
14
|
+
"types": "./dist/update.d.ts",
|
|
15
|
+
"default": "./dist/update.js"
|
|
16
|
+
},
|
|
17
|
+
"./update/client": {
|
|
18
|
+
"types": "./dist/update-client.d.ts",
|
|
19
|
+
"default": "./dist/update-client.js"
|
|
20
|
+
},
|
|
21
|
+
"./http": {
|
|
22
|
+
"types": "./dist/http.d.ts",
|
|
23
|
+
"default": "./dist/http.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"package.json"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": "^22.20.0 || >=24.0.0"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"dsh",
|
|
38
|
+
"dsh-plugin",
|
|
39
|
+
"deepseek-harness",
|
|
40
|
+
"plugin-update",
|
|
41
|
+
"self-update"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
47
|
+
"pretest": "npm run build",
|
|
48
|
+
"check": "npm run typecheck && npm run build && npm test --ignore-scripts"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "22.20.5",
|
|
52
|
+
"tsup": "8.5.1",
|
|
53
|
+
"typescript": "5.9.3"
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"author": "morehao <morehao@qq.com>",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/morehao/dsh-plugin-kit.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/morehao/dsh-plugin-kit/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/morehao/dsh-plugin-kit",
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public",
|
|
67
|
+
"registry": "https://registry.npmjs.org/"
|
|
68
|
+
}
|
|
69
|
+
}
|