@morehao/dsh-plugin-kit 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -14,16 +14,34 @@ DeepSeek Harness(dsh)插件共用的工具包。当前提供三件事:
14
14
  npm install @morehao/dsh-plugin-kit
15
15
  ```
16
16
 
17
+ ## 能力边界:哪些复用宿主、哪些必须自建
18
+
19
+ 自更新这件事在宿主(`@deepseek-ai/dsh-plugin-manager`)里已有大半能力,本包的原则是**能复用就复用**:
20
+
21
+ | 能力 | 来源 | 说明 |
22
+ |---|---|---|
23
+ | 安装 / 卸载 | **复用宿主** | apply 全程走 `pluginManager.installBundle` / `removeBundle`;`already-installed → remove → install` 就是宿主既定的更新流程,`applied/restart-required/overridden` 也直接来自宿主结果。 |
24
+ | registry 列表(镜像 / 私源 / token) | **复用宿主** | `resolved` 由宿主跑 `pnpm config get registry` 解析,本包不自己读 `.npmrc`。 |
25
+ | registry **排序 / 回退** | **复用宿主(注入)** | 优先用调用方注入的宿主原生 `registryPlan`(`@deepseek-ai/dsh-plugin-manager/registry`),与 `installBundle`/`inspect` 内部同一份源码、含私有源「只问它自己、不回退公有源」语义。本包不直接 import(保持零 `@deepseek-ai/*` 运行时依赖),由插件侧注入。未注入时用内置 `resolveRegistryPlan` 兜底(`resolved → registry → fallback` 宽松顺序,fail-open)。 |
26
+ | 查最新版 | **必须自建** | 宿主 `inspect()` 对「已装包」直接返回 `already-installed`,不会去问 registry;插件查的正是「我自己这个已装包」,必然被拦截。故只能走 `pnpm view`(`runPnpmView`,复用 profile 的包管理器)或 registry `/latest`。 |
27
+ | 安装形态判定 | **必须自建** | 宿主未暴露「本包以什么 spec 声明」;本包读 `<profile.dir>/package.json` 识别 `link:/file:/workspace:` 以拒绝把开发链接覆盖成 npm 版。 |
28
+
29
+ 一句话:**装/卸、registry 取数与排序都复用宿主(排序原生能力通过注入复用);查版本、判形态因宿主 API 缺口而自建;不注入原生 `registryPlan` 时才回落到内置兜底顺序。**
30
+
17
31
  ## 一键更新(host)
18
32
 
19
33
  ```ts
20
34
  import { createPluginUpdater } from '@morehao/dsh-plugin-kit/update';
35
+ // 宿主原生 registry 排序(与 installBundle/inspect 同一份),由插件侧注入、本包不直接依赖
36
+ import { registryPlan as hostRegistryPlan } from '@deepseek-ai/dsh-plugin-manager/registry';
21
37
 
22
38
  const updater = createPluginUpdater({
23
39
  meta: { name: pkg.name, version: pkg.version, ...(pkg.repository ? { repository: repoUrl(pkg.repository) } : {}) },
24
40
  getPluginManager: () => ctx.get('pluginManager'),
25
41
  getProfile: () => ctx.get('profileContext'),
26
42
  getProfileName: () => ctx.get('profileContext')?.name,
43
+ // 可选但推荐:注入宿主原生 registryPlan,使「查版本的 registry 顺序」与宿主安装完全一致
44
+ registryPlan: hostRegistryPlan,
27
45
  // 可选:用 profile 的包管理器查版本(继承 .npmrc 的镜像 / 私源 / token)
28
46
  runPnpmView: async (spec) => {
29
47
  const profile = ctx.get('profileContext');
@@ -106,5 +124,5 @@ sendJson(response, 200, { ok: true });
106
124
 
107
125
  ```bash
108
126
  npm install
109
- npm run check # typecheck + build + 24 条离线测试(假 fetch / 假 pluginManager,不联网、不真装)
127
+ npm run check # typecheck + build + 26 条离线测试(假 fetch / 假 pluginManager,不联网、不真装)
110
128
  ```
package/dist/index.d.ts CHANGED
@@ -1,43 +1,15 @@
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';
1
+ export { D as DEFAULT_CACHE_TTL_MS, a as DEFAULT_TIMEOUT_MS, b as DetectDeps, H as HostRegistryPlan, I as InstallKind, c as InstallKindResult, O as OFFICIAL_REGISTRY, P as ProfileLike, R as RegistriesLike, d as classifyInstallSpec, e as detectInstallKind, f as encodePackageName, m as manualCommand, n as normalizeProfile, g as normalizeRegistry, r as readDeclaredSpec, h as registryPlan, i as resolveRegistryPlan } from './profile-RKpxKDlK.js';
2
2
 
3
- /**
4
- * semver 解析与比较(只做插件更新需要的部分)。
5
- *
6
- * 不引 semver 依赖:更新判定宁可不升级也不要误升,所以非法版本一律返回 undefined,
7
- * 由调用方按「不更新」处理。支持 `v1.2.3` / `1.2.3-beta.1` / `1.2.3+build`。
8
- */
3
+ /** 保留旧接口形状:核心三段 + 预发布标识(字符串数组)。 */
9
4
  interface Semver {
10
5
  core: [number, number, number];
11
6
  prerelease: string[];
12
7
  }
8
+ /** 解析成 `Semver`;非法(含空串 / `1.2` / `1.2.3.4` / 非版本串)返回 undefined。 */
13
9
  declare function parseSemver(value: unknown): Semver | undefined;
14
10
  /** `candidate` 是否严格新于 `current`;任一方不是合法 semver 都返回 false。 */
15
11
  declare function isNewerVersion(current: unknown, candidate: unknown): boolean;
16
12
  /** 比较两个版本:负数 / 0 / 正数;任一方非法返回 undefined。 */
17
13
  declare function compareVersions(left: unknown, right: unknown): number | undefined;
18
14
 
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 };
15
+ export { type Semver, compareVersions, isNewerVersion, parseSemver };
package/dist/index.js CHANGED
@@ -1,59 +1,29 @@
1
1
  // src/semver.ts
2
- var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
2
+ import semver from "semver";
3
3
  function parseSemver(value) {
4
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;
5
+ if (text === "") return void 0;
6
+ const parsed = semver.parse(text);
7
+ if (parsed === null) return void 0;
8
+ return {
9
+ core: [parsed.major, parsed.minor, parsed.patch],
10
+ // semver 会把纯数字预发布段解析为 number,这里统一转回字符串以保持旧契约。
11
+ prerelease: parsed.prerelease.map((part) => String(part))
12
+ };
35
13
  }
36
14
  function isNewerVersion(current, candidate) {
37
15
  const a = parseSemver(current);
38
16
  const b = parseSemver(candidate);
39
17
  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;
18
+ const left = String(current).trim();
19
+ const right = String(candidate).trim();
20
+ return semver.gt(right, left);
46
21
  }
47
22
  function compareVersions(left, right) {
48
23
  const a = parseSemver(left);
49
24
  const b = parseSemver(right);
50
25
  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);
26
+ return semver.compare(String(left).trim(), String(right).trim());
57
27
  }
58
28
 
59
29
  // src/registry.ts
@@ -96,6 +66,36 @@ function registryPlan(registries) {
96
66
  if (ordered.length === 0) ordered.push(OFFICIAL_REGISTRY);
97
67
  return ordered;
98
68
  }
69
+ function planFromHost(hostPlan, registries) {
70
+ if (registries === null || typeof registries !== "object") return null;
71
+ const ordered = [];
72
+ const push = (value) => {
73
+ const normalized = normalizeRegistry(value);
74
+ if (normalized !== null && !ordered.includes(normalized)) ordered.push(normalized);
75
+ };
76
+ let result;
77
+ try {
78
+ result = hostPlan(void 0, registries);
79
+ } catch {
80
+ return null;
81
+ }
82
+ if (!Array.isArray(result)) return null;
83
+ for (const entry of result) {
84
+ if (entry === null) {
85
+ push(registries.resolved);
86
+ continue;
87
+ }
88
+ push(entry);
89
+ }
90
+ return ordered.length === 0 ? null : ordered;
91
+ }
92
+ function resolveRegistryPlan(registries, hostPlan) {
93
+ if (typeof hostPlan === "function") {
94
+ const fromHost = planFromHost(hostPlan, registries);
95
+ if (fromHost !== null) return fromHost;
96
+ }
97
+ return registryPlan(registries);
98
+ }
99
99
  function manualCommand(packageName, profileName) {
100
100
  const spec = `${packageName}@latest`;
101
101
  return typeof profileName === "string" && profileName !== "" ? `dsh plugin --profile ${profileName} add ${spec}` : `dsh plugin add ${spec}`;
@@ -175,5 +175,6 @@ export {
175
175
  normalizeRegistry,
176
176
  parseSemver,
177
177
  readDeclaredSpec,
178
- registryPlan
178
+ registryPlan,
179
+ resolveRegistryPlan
179
180
  };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * npm registry 约定:把包名拼成 `/latest` 端点,并按宿主给出的 registry 优先级发请求。
3
+ *
4
+ * **registry 列表与排序都优先复用宿主原生能力**(`@deepseek-ai/dsh-plugin-manager/registry` 的
5
+ * `registryPlan(requested, configured)`,与 `installBundle` / `inspect` 内部用的是同一份源码)。
6
+ * 但本包坚持**零 `@deepseek-ai/*` 运行时依赖**(见 package.json / update.ts),所以不直接 import,
7
+ * 而是让调用方把原生函数**注入**进来(见 `HostRegistryPlan` / `LatestDeps.registryPlan`):
8
+ *
9
+ * - 注入了 → 用宿主原生排序(含私有源「只问它自己、不回退公有源」的语义,与安装完全一致);
10
+ * - 没注入 / 调用抛错 / 结果不可用 → 退回本模块内置的 `registryPlan` 兜底(见下)。
11
+ *
12
+ * 为什么查最新版不复用 `pluginManager.inspect()`:`inspect(spec)` 在 registry 分支里先判
13
+ * 「该包是否已在 profile 依赖中」,命中即返回 `already-installed`,**根本不会去问 registry**。
14
+ * 而插件自更新要查的恰恰是「我自己这个已装包有没有新版」,必然命中该拦截。所以查 latest
15
+ * 只能另走 `pnpm view`(见 `latest.ts` 的 `runPnpmView`)或 registry `/latest`,`inspect` 不可替代。
16
+ */
17
+ declare const OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
18
+ declare const DEFAULT_TIMEOUT_MS = 8000;
19
+ declare const DEFAULT_CACHE_TTL_MS: number;
20
+ /**
21
+ * 宿主 `pluginManager.registries()` 的返回形状(对齐 @deepseek-ai/dsh-plugin-manager)。
22
+ *
23
+ * 设计取舍:字段都按可选 + unknown 收。宿主版本差异或字段改名时,内置兜底 `registryPlan` 用
24
+ * `normalizeRegistry` 逐个校验,非法项直接跳过并最终退到官方源,**绝不因形状不符而抛错**
25
+ * (fail-open:宁可退化到官方源,也不让自更新检查因宿主形状变化而崩)。
26
+ */
27
+ interface RegistriesLike {
28
+ /** pnpm 在 profile 内解析出的实际 registry(继承 .npmrc),非空时最优先。 */
29
+ resolved?: unknown;
30
+ /** 配置里显式的 registry(可能为 null,表示「pnpm 自身配置的源」)。 */
31
+ registry?: unknown;
32
+ /** 回退源;宿主默认含 npmmirror。 */
33
+ fallbackRegistries?: unknown;
34
+ }
35
+ /**
36
+ * 宿主原生 `registryPlan` 的注入形状,对齐 `@deepseek-ai/dsh-plugin-manager/registry`。
37
+ *
38
+ * 调用方(各插件)自己 `import { registryPlan } from '@deepseek-ai/dsh-plugin-manager/registry'`
39
+ * 后注入本包,即可让「查最新版的 registry 顺序」与宿主安装使用**完全一致**的排序与私有源语义。
40
+ *
41
+ * 入参/返回:
42
+ * - `requested`:首位要问的 registry;`undefined` 表示交给宿主按 configured 首项决定。
43
+ * - `configured`:`registries()` 的返回值(resolved / registry / fallbackRegistries)。
44
+ * - 返回:依次要问的 registry 数组,**元素可能为 `null`**(表示 pnpm 自身配置的源);宿主实现
45
+ * 在入参非法时会**抛错**,所以本包调用处已包 try/catch 兜底。
46
+ */
47
+ type HostRegistryPlan = (requested: string | undefined, configured: RegistriesLike) => Array<string | null>;
48
+ /** npm 包名转 registry 路径:`@scope/name` → `@scope%2Fname`(保留 @,斜杠编码)。 */
49
+ declare function encodePackageName(name: string): string;
50
+ /** registry 归一成 `https://host/` 形式;非法(非 http(s) / 解析失败)返回 null。 */
51
+ declare function normalizeRegistry(value: unknown): string | null;
52
+ /**
53
+ * 内置兜底:把 `registries()` 的结果收敛成「查 latest 时要依次试的 registry 顺序」:
54
+ * `resolved → registry → fallback`,去重;全非法/缺失时退到官方 registry,**绝不抛**。
55
+ *
56
+ * 这是**注入不到宿主原生 `registryPlan` 时的简化兜底**,不是宿主安装计划的等价物:它不区分
57
+ * 公有/私有源。因此若 `resolved` 是私有源而其上查不到本包(镜像未同步),兜底会继续去问
58
+ * fallback / 官方源拿 latest;而宿主真正安装时可能只问私有源并失败。差异只影响「能否查到版本」,
59
+ * 不影响实际安装走哪条路。想与宿主机安装完全对齐,请注入原生 `registryPlan`(见 `HostRegistryPlan`)。
60
+ */
61
+ declare function registryPlan(registries: unknown): string[];
62
+ /**
63
+ * 解析「查 latest 时要依次试的 registry 顺序」:
64
+ * **优先用宿主原生 `registryPlan`**(注入且成功时,排序/私有源语义与宿主机安装完全一致),
65
+ * 否则退回内置 `registryPlan` 兜底(fail-open,保证查询永远有一条可走的路)。
66
+ *
67
+ * @param registries 宿主 `registries()` 的返回值(任意形状,内部按 unknown 校验)。
68
+ * @param hostPlan 可选的宿主原生 `registryPlan`(由调用方从 dsh-plugin-manager/registry 注入)。
69
+ */
70
+ declare function resolveRegistryPlan(registries: unknown, hostPlan?: HostRegistryPlan): string[];
71
+ /** 手动更新命令:`dsh plugin --profile <name> add <pkg>@latest`。 */
72
+ declare function manualCommand(packageName: string, profileName?: string | null): string;
73
+
74
+ /**
75
+ * profile 读取:判断本插件是以什么形态装进当前 profile。
76
+ *
77
+ * 一键更新最危险的场景是「开发用的本地链接」:profile 里写的是 `link:/path/to/repo`,
78
+ * 一旦用 registry 重装,本地的 link 会被换成 npm 版,开发流就被悄悄改掉了。所以
79
+ * 更新前先读 `<profile.dir>/package.json` 的依赖声明,link: / file: / workspace: 一律
80
+ * 关闭一键更新,只给手动命令。
81
+ */
82
+ type InstallKind = 'registry' | 'link' | 'file' | 'workspace' | 'unknown';
83
+ interface ProfileLike {
84
+ name?: string;
85
+ dir?: string;
86
+ home?: string;
87
+ packageManager?: {
88
+ command?: string;
89
+ args?: string[];
90
+ };
91
+ }
92
+ interface InstallKindResult {
93
+ kind: InstallKind;
94
+ /** profile package.json 里声明的原始 spec(读不到就是 undefined)。 */
95
+ spec?: string;
96
+ /** 说明来源,便于日志与界面提示。 */
97
+ source?: string;
98
+ }
99
+ /** 把宿主 `profileContext`(任意形状)收敛成只含我们关心的字段;拿不到返回 null。 */
100
+ declare function normalizeProfile(value: unknown): ProfileLike | null;
101
+ /** `dependencies` 里声明的 spec 决定安装形态;`npm:` 别名仍按 registry 处理。 */
102
+ declare function classifyInstallSpec(spec: unknown): InstallKind;
103
+ interface DetectDeps {
104
+ profile: ProfileLike | null;
105
+ packageName: string;
106
+ /** 测试注入;默认读真实文件。 */
107
+ readFile?: (path: string) => Promise<string>;
108
+ readJson?: (path: string) => Promise<unknown>;
109
+ }
110
+ /** 读 profile 的 package.json,返回本插件声明的 spec(找不到返回 undefined)。 */
111
+ declare function readDeclaredSpec(deps: DetectDeps): Promise<{
112
+ spec?: string;
113
+ source?: string;
114
+ }>;
115
+ declare function detectInstallKind(deps: DetectDeps): Promise<InstallKindResult>;
116
+
117
+ export { DEFAULT_CACHE_TTL_MS as D, type HostRegistryPlan as H, type InstallKind as I, OFFICIAL_REGISTRY as O, type ProfileLike as P, type RegistriesLike as R, DEFAULT_TIMEOUT_MS as a, type DetectDeps as b, type InstallKindResult as c, classifyInstallSpec as d, detectInstallKind as e, encodePackageName as f, normalizeRegistry as g, registryPlan as h, resolveRegistryPlan as i, manualCommand as m, normalizeProfile as n, readDeclaredSpec as r };
@@ -1,5 +1,5 @@
1
1
  import { UpdateView, ApplyResult } from './update.js';
2
- import './profile-C-eyP5F6.js';
2
+ import './profile-RKpxKDlK.js';
3
3
 
4
4
  /**
5
5
  * 客户端侧 headless 控制器(不依赖 React、不 import node 内置模块)。
package/dist/update.d.ts CHANGED
@@ -1,5 +1,25 @@
1
- import { I as InstallKind } from './profile-C-eyP5F6.js';
2
- export { P as ProfileLike } from './profile-C-eyP5F6.js';
1
+ import { I as InstallKind, H as HostRegistryPlan } from './profile-RKpxKDlK.js';
2
+ export { P as ProfileLike } from './profile-RKpxKDlK.js';
3
+
4
+ /**
5
+ * 查 npm 上某个包的最新版本。
6
+ *
7
+ * 顺序(可配置):
8
+ * 1. 调用方注入的 `runPnpmView`(让插件用自己 profile 的包管理器查,继承 .npmrc 的
9
+ * 镜像 / 私源 / token;宿主内裸 fetch 读不到这些配置)。**这是复用宿主包管理器**:
10
+ * 查询在 profile 目录、profile 的 packageManager 环境下执行,与宿主安装同源。
11
+ * 2. 宿主 `pluginManager.registries()` 给出的 registry 列表,依次问 `<base><pkg>/latest`;
12
+ * 排序**优先用调用方注入的宿主原生 `registryPlan`**(与安装完全一致),未注入则内置兜底
13
+ * (见 `registry.ts` 的 `resolveRegistryPlan`);
14
+ * 3. 都没有就用官方 registry。
15
+ *
16
+ * **为什么不用宿主的 `pluginManager.inspect()` 查最新版**:`inspect()` 在 registry 分支会先判
17
+ * 「该包是否已在 profile 依赖中」,命中即返回 `already-installed`,不会真正去问 registry。
18
+ * 插件自更新要查的正是「我自己这个已装包」,必然命中该拦截,所以此处只能走 `pnpm view` / `/latest`。
19
+ *
20
+ * 全部失败返回 null(不抛):调用方用它标记 `checkFailed`,界面照常可用。注意「查不到」≠「宿主缺失」,
21
+ * 而是网络 / registry / 包管理器任一环节不可用时的运行时降级。
22
+ */
3
23
 
4
24
  type Logger = (event: string, detail?: Record<string, unknown>) => void;
5
25
  interface PluginMeta {
@@ -13,7 +33,11 @@ interface PluginMeta {
13
33
  *
14
34
  * 设计约束(三个 DSH 插件共用这份逻辑):
15
35
  * - **零 `@deepseek-ai/*` 运行时依赖**:pluginManager / profileContext 都由调用方用
16
- * `ctx.get(...)` 惰性取,拿不到就退化成「只检查 + 手动命令」。
36
+ * `ctx.get(...)` 惰性取。本包定位就是 DSH 插件,宿主必然在;但 `installBundle` 是
37
+ * 可选/远程能力,取不到只是「当前部署未暴露该能力」,退化成「只检查 + 手动命令」。
38
+ * - **复用优先**:registry 顺序优先用调用方注入的宿主原生 `registryPlan`(与宿主安装完全一致,
39
+ * 含私有源语义),未注入则内置兜底(见 `registry.ts` 的 `resolveRegistryPlan`);查最新版走
40
+ * `pnpm view` / registry `/latest`(`inspect()` 对已装包返回 already-installed,不可替代)。
17
41
  * - **不自作主张升级**:只有 registry 明确给出合法且严格更新的版本才允许安装;
18
42
  * `link:` / `file:` / `workspace:` 安装一律禁止一键更新(会把开发链接换成 npm 版)。
19
43
  * - **装不上要能回退**:宿主 `installBundle` 对已装包会返回 `already-installed`,按官方
@@ -63,6 +87,8 @@ interface UpdaterDeps {
63
87
  getProfileName?: () => string | null;
64
88
  fetchImpl?: typeof fetch;
65
89
  runPnpmView?: (spec: string) => Promise<string | null>;
90
+ /** 可选:注入宿主原生 `registryPlan`(dsh-plugin-manager/registry),让查版本的 registry 顺序与安装一致。 */
91
+ registryPlan?: HostRegistryPlan;
66
92
  readFile?: (path: string) => Promise<string>;
67
93
  logger?: Logger;
68
94
  now?: () => number;
@@ -84,4 +110,4 @@ interface Updater {
84
110
  }
85
111
  declare function createPluginUpdater(deps: UpdaterDeps): Updater;
86
112
 
87
- export { type ApplyResult, InstallKind, type Logger, type PluginManagerLike, type PluginMeta, PluginUpdateError, type UpdateErrorCode, type UpdateView, type Updater, type UpdaterDeps, createPluginUpdater };
113
+ export { type ApplyResult, HostRegistryPlan, InstallKind, type Logger, type PluginManagerLike, type PluginMeta, PluginUpdateError, type UpdateErrorCode, type UpdateView, type Updater, type UpdaterDeps, createPluginUpdater };
package/dist/update.js CHANGED
@@ -38,56 +38,61 @@ function registryPlan(registries) {
38
38
  if (ordered.length === 0) ordered.push(OFFICIAL_REGISTRY);
39
39
  return ordered;
40
40
  }
41
+ function planFromHost(hostPlan, registries) {
42
+ if (registries === null || typeof registries !== "object") return null;
43
+ const ordered = [];
44
+ const push = (value) => {
45
+ const normalized = normalizeRegistry(value);
46
+ if (normalized !== null && !ordered.includes(normalized)) ordered.push(normalized);
47
+ };
48
+ let result;
49
+ try {
50
+ result = hostPlan(void 0, registries);
51
+ } catch {
52
+ return null;
53
+ }
54
+ if (!Array.isArray(result)) return null;
55
+ for (const entry of result) {
56
+ if (entry === null) {
57
+ push(registries.resolved);
58
+ continue;
59
+ }
60
+ push(entry);
61
+ }
62
+ return ordered.length === 0 ? null : ordered;
63
+ }
64
+ function resolveRegistryPlan(registries, hostPlan) {
65
+ if (typeof hostPlan === "function") {
66
+ const fromHost = planFromHost(hostPlan, registries);
67
+ if (fromHost !== null) return fromHost;
68
+ }
69
+ return registryPlan(registries);
70
+ }
41
71
  function manualCommand(packageName, profileName) {
42
72
  const spec = `${packageName}@latest`;
43
73
  return typeof profileName === "string" && profileName !== "" ? `dsh plugin --profile ${profileName} add ${spec}` : `dsh plugin add ${spec}`;
44
74
  }
45
75
 
46
76
  // src/semver.ts
47
- var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
77
+ import semver from "semver";
48
78
  function parseSemver(value) {
49
79
  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
+ if (text === "") return void 0;
81
+ const parsed = semver.parse(text);
82
+ if (parsed === null) return void 0;
83
+ return {
84
+ core: [parsed.major, parsed.minor, parsed.patch],
85
+ // semver 会把纯数字预发布段解析为 number,这里统一转回字符串以保持旧契约。
86
+ prerelease: parsed.prerelease.map((part) => String(part))
87
+ };
80
88
  }
81
89
  function isNewerVersion(current, candidate) {
82
90
  const a = parseSemver(current);
83
91
  const b = parseSemver(candidate);
84
92
  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;
93
+ const left = String(current).trim();
94
+ const right = String(candidate).trim();
95
+ return semver.gt(right, left);
91
96
  }
92
97
 
93
98
  // src/latest.ts
@@ -131,7 +136,7 @@ function createLatestResolver(deps) {
131
136
  }
132
137
  }
133
138
  async function viaRegistries() {
134
- const plan = registryPlan(await readRegistries());
139
+ const plan = resolveRegistryPlan(await readRegistries(), deps.registryPlan);
135
140
  for (const base of plan) {
136
141
  try {
137
142
  const response = await fetchImpl(`${base}${encodePackageName(deps.meta.name)}/latest`, {
@@ -273,6 +278,7 @@ function createPluginUpdater(deps) {
273
278
  ...deps.getPluginManager === void 0 ? {} : { getPluginManager: deps.getPluginManager },
274
279
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
275
280
  ...deps.runPnpmView === void 0 ? {} : { runPnpmView: deps.runPnpmView },
281
+ ...deps.registryPlan === void 0 ? {} : { registryPlan: deps.registryPlan },
276
282
  ...deps.now === void 0 ? {} : { now: deps.now },
277
283
  ...deps.cacheTtlMs === void 0 ? {} : { cacheTtlMs: deps.cacheTtlMs },
278
284
  ...deps.timeoutMs === void 0 ? {} : { timeoutMs: deps.timeoutMs },
@@ -376,7 +382,7 @@ function createPluginUpdater(deps) {
376
382
  }
377
383
  const service = manager();
378
384
  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", {
385
+ throw new PluginUpdateError("NO_MANAGER", "\u5F53\u524D profile \u672A\u63D0\u4F9B\u4E00\u952E\u5B89\u88C5\u80FD\u529B\uFF08pluginManager.installBundle \u4E0D\u53EF\u7528\uFF09\uFF0C\u8BF7\u624B\u52A8\u66F4\u65B0", {
380
386
  manualCommand: view.manualCommand
381
387
  });
382
388
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-plugin-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "DeepSeek Harness 插件共用工具包:插件自更新引擎(查 npm 最新版 + 经宿主 pluginManager 重装)、headless 客户端控制器与 HTTP 小工具。零 @deepseek-ai 运行时依赖,宿主能力全部注入。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "22.20.5",
52
+ "@types/semver": "^7.8.0",
52
53
  "tsup": "8.5.1",
53
54
  "typescript": "5.9.3"
54
55
  },
@@ -65,5 +66,8 @@
65
66
  "publishConfig": {
66
67
  "access": "public",
67
68
  "registry": "https://registry.npmjs.org/"
69
+ },
70
+ "dependencies": {
71
+ "semver": "^7.8.5"
68
72
  }
69
73
  }
@@ -1,44 +0,0 @@
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 };