@morehao/dsh-plugin-kit 0.1.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/dist/update.js CHANGED
@@ -1,3 +1,170 @@
1
+ // src/exec.ts
2
+ import { spawn } from "child_process";
3
+ var ENV_KEYS = [
4
+ "PATH",
5
+ "HOME",
6
+ "USER",
7
+ "LOGNAME",
8
+ "SHELL",
9
+ "TMPDIR",
10
+ "TMP",
11
+ "TEMP",
12
+ "LANG",
13
+ "LC_ALL",
14
+ "LC_CTYPE",
15
+ "SSL_CERT_FILE",
16
+ "SSL_CERT_DIR",
17
+ "GIT_AUTHOR_NAME",
18
+ "GIT_AUTHOR_EMAIL",
19
+ "GIT_COMMITTER_NAME",
20
+ "GIT_COMMITTER_EMAIL",
21
+ "GIT_CONFIG_GLOBAL",
22
+ "GIT_SSH_COMMAND"
23
+ ];
24
+ var PROXY_KEYS = [
25
+ "HTTP_PROXY",
26
+ "HTTPS_PROXY",
27
+ "NO_PROXY",
28
+ "ALL_PROXY",
29
+ "http_proxy",
30
+ "https_proxy",
31
+ "no_proxy",
32
+ "all_proxy"
33
+ ];
34
+ function sanitizedEnv(source = process.env, extra = {}) {
35
+ const env = {};
36
+ for (const key of [...ENV_KEYS, ...PROXY_KEYS]) {
37
+ const value = source[key];
38
+ if (typeof value === "string" && value !== "") env[key] = value;
39
+ }
40
+ for (const [key, value] of Object.entries(extra)) {
41
+ if (typeof value === "string" && value !== "") env[key] = value;
42
+ }
43
+ return env;
44
+ }
45
+ function keepTail(text, limit) {
46
+ if (!Number.isFinite(limit) || limit <= 0 || text.length <= limit) return text;
47
+ return text.slice(text.length - limit);
48
+ }
49
+ async function spawnCollect(command, args = [], options = {}) {
50
+ const started = Date.now();
51
+ const timeoutMs = options.timeoutMs ?? 1e4;
52
+ const abortGraceMs = options.abortGraceMs ?? 2e3;
53
+ const maxStdout = options.maxStdout ?? 1024 * 1024;
54
+ const maxStderr = options.maxStderr ?? 1024 * 1024;
55
+ const maxOutput = options.maxOutput ?? maxStdout + maxStderr;
56
+ let stdout = "";
57
+ let stderr = "";
58
+ let output = "";
59
+ let timedOut = false;
60
+ return new Promise((resolve) => {
61
+ let child;
62
+ try {
63
+ child = spawn(command, args, {
64
+ cwd: options.cwd,
65
+ env: options.env ?? process.env,
66
+ stdio: [options.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
67
+ windowsHide: true
68
+ });
69
+ } catch (error) {
70
+ resolve(failureResult(error, started, options.signal?.aborted === true));
71
+ return;
72
+ }
73
+ if (options.input !== void 0) {
74
+ child.stdin?.on("error", () => void 0);
75
+ child.stdin?.end(options.input);
76
+ }
77
+ let settled = false;
78
+ let timer;
79
+ let hardKill;
80
+ const killTree = (signal) => {
81
+ try {
82
+ child.kill(signal);
83
+ } catch {
84
+ }
85
+ };
86
+ const softKill = () => {
87
+ killTree("SIGTERM");
88
+ hardKill = setTimeout(() => killTree("SIGKILL"), abortGraceMs);
89
+ hardKill.unref?.();
90
+ };
91
+ const onAbort = () => softKill();
92
+ const finish = (code, signal, error) => {
93
+ if (settled) return;
94
+ settled = true;
95
+ if (timer !== void 0) clearTimeout(timer);
96
+ if (hardKill !== void 0) clearTimeout(hardKill);
97
+ options.signal?.removeEventListener("abort", onAbort);
98
+ resolve({
99
+ code,
100
+ signal,
101
+ stdout,
102
+ stderr,
103
+ output,
104
+ timedOut,
105
+ aborted: options.signal?.aborted === true,
106
+ durationMs: Date.now() - started,
107
+ ...error === void 0 ? {} : { error }
108
+ });
109
+ };
110
+ child.stdout?.setEncoding("utf8");
111
+ child.stdout?.on("data", (chunk) => {
112
+ stdout = keepTail(stdout + chunk, maxStdout);
113
+ output = keepTail(output + chunk, maxOutput);
114
+ try {
115
+ options.onData?.(chunk, "stdout");
116
+ } catch {
117
+ }
118
+ });
119
+ child.stderr?.setEncoding("utf8");
120
+ child.stderr?.on("data", (chunk) => {
121
+ stderr = keepTail(stderr + chunk, maxStderr);
122
+ output = keepTail(output + chunk, maxOutput);
123
+ try {
124
+ options.onData?.(chunk, "stderr");
125
+ } catch {
126
+ }
127
+ });
128
+ if (options.signal?.aborted === true) onAbort();
129
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
130
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
131
+ timer = setTimeout(() => {
132
+ timedOut = true;
133
+ softKill();
134
+ }, timeoutMs);
135
+ }
136
+ child.on("error", (error) => finish(null, null, error));
137
+ child.on("close", (code, signalName) => finish(code, signalName));
138
+ });
139
+ }
140
+ function failureResult(error, started, aborted) {
141
+ const err = error instanceof Error ? error : new Error(String(error));
142
+ return {
143
+ code: null,
144
+ signal: null,
145
+ stdout: "",
146
+ stderr: "",
147
+ output: err.message,
148
+ timedOut: false,
149
+ aborted,
150
+ durationMs: Date.now() - started,
151
+ error: err
152
+ };
153
+ }
154
+ function collectVersionView(result) {
155
+ if (result.code !== 0) return null;
156
+ const text = result.stdout.trim();
157
+ if (text === "") return null;
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(text);
161
+ } catch {
162
+ return null;
163
+ }
164
+ const value = Array.isArray(parsed) ? parsed[parsed.length - 1] : parsed;
165
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
166
+ }
167
+
1
168
  // src/registry.ts
2
169
  var OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
3
170
  var DEFAULT_TIMEOUT_MS = 8e3;
@@ -38,56 +205,61 @@ function registryPlan(registries) {
38
205
  if (ordered.length === 0) ordered.push(OFFICIAL_REGISTRY);
39
206
  return ordered;
40
207
  }
208
+ function planFromHost(hostPlan, registries) {
209
+ if (registries === null || typeof registries !== "object") return null;
210
+ const ordered = [];
211
+ const push = (value) => {
212
+ const normalized = normalizeRegistry(value);
213
+ if (normalized !== null && !ordered.includes(normalized)) ordered.push(normalized);
214
+ };
215
+ let result;
216
+ try {
217
+ result = hostPlan(void 0, registries);
218
+ } catch {
219
+ return null;
220
+ }
221
+ if (!Array.isArray(result)) return null;
222
+ for (const entry of result) {
223
+ if (entry === null) {
224
+ push(registries.resolved);
225
+ continue;
226
+ }
227
+ push(entry);
228
+ }
229
+ return ordered.length === 0 ? null : ordered;
230
+ }
231
+ function resolveRegistryPlan(registries, hostPlan) {
232
+ if (typeof hostPlan === "function") {
233
+ const fromHost = planFromHost(hostPlan, registries);
234
+ if (fromHost !== null) return fromHost;
235
+ }
236
+ return registryPlan(registries);
237
+ }
41
238
  function manualCommand(packageName, profileName) {
42
239
  const spec = `${packageName}@latest`;
43
240
  return typeof profileName === "string" && profileName !== "" ? `dsh plugin --profile ${profileName} add ${spec}` : `dsh plugin add ${spec}`;
44
241
  }
45
242
 
46
243
  // src/semver.ts
47
- var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
244
+ import semver from "semver";
48
245
  function parseSemver(value) {
49
246
  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;
247
+ if (text === "") return void 0;
248
+ const parsed = semver.parse(text);
249
+ if (parsed === null) return void 0;
250
+ return {
251
+ core: [parsed.major, parsed.minor, parsed.patch],
252
+ // semver 会把纯数字预发布段解析为 number,这里统一转回字符串以保持旧契约。
253
+ prerelease: parsed.prerelease.map((part) => String(part))
254
+ };
80
255
  }
81
256
  function isNewerVersion(current, candidate) {
82
257
  const a = parseSemver(current);
83
258
  const b = parseSemver(candidate);
84
259
  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;
260
+ const left = String(current).trim();
261
+ const right = String(candidate).trim();
262
+ return semver.gt(right, left);
91
263
  }
92
264
 
93
265
  // src/latest.ts
@@ -131,7 +303,7 @@ function createLatestResolver(deps) {
131
303
  }
132
304
  }
133
305
  async function viaRegistries() {
134
- const plan = registryPlan(await readRegistries());
306
+ const plan = resolveRegistryPlan(await readRegistries(), deps.registryPlan);
135
307
  for (const base of plan) {
136
308
  try {
137
309
  const response = await fetchImpl(`${base}${encodePackageName(deps.meta.name)}/latest`, {
@@ -273,6 +445,7 @@ function createPluginUpdater(deps) {
273
445
  ...deps.getPluginManager === void 0 ? {} : { getPluginManager: deps.getPluginManager },
274
446
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
275
447
  ...deps.runPnpmView === void 0 ? {} : { runPnpmView: deps.runPnpmView },
448
+ ...deps.registryPlan === void 0 ? {} : { registryPlan: deps.registryPlan },
276
449
  ...deps.now === void 0 ? {} : { now: deps.now },
277
450
  ...deps.cacheTtlMs === void 0 ? {} : { cacheTtlMs: deps.cacheTtlMs },
278
451
  ...deps.timeoutMs === void 0 ? {} : { timeoutMs: deps.timeoutMs },
@@ -376,7 +549,7 @@ function createPluginUpdater(deps) {
376
549
  }
377
550
  const service = manager();
378
551
  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", {
552
+ 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
553
  manualCommand: view.manualCommand
381
554
  });
382
555
  }
@@ -453,7 +626,38 @@ function createPluginUpdater(deps) {
453
626
  isApplying: () => applying
454
627
  };
455
628
  }
629
+ function createPnpmViewRunner(deps) {
630
+ const timeoutMs = deps.timeoutMs ?? 8e3;
631
+ const run = deps.exec ?? spawnCollect;
632
+ const env = sanitizedEnv(deps.env ?? process.env);
633
+ return async (spec) => {
634
+ let profile;
635
+ try {
636
+ profile = normalizeProfile(deps.getProfile());
637
+ } catch {
638
+ return null;
639
+ }
640
+ if (profile === null || typeof profile.dir !== "string" || profile.dir === "") return null;
641
+ const command = profile.packageManager?.command ?? "pnpm";
642
+ const prefix = profile.packageManager?.args ?? [];
643
+ let result;
644
+ try {
645
+ result = await run(command, [...prefix, "view", spec, "version", "--json"], {
646
+ cwd: profile.dir,
647
+ env,
648
+ timeoutMs,
649
+ maxStdout: 64 * 1024,
650
+ maxStderr: 16 * 1024
651
+ });
652
+ } catch {
653
+ return null;
654
+ }
655
+ const version = collectVersionView(result);
656
+ return version !== null && parseSemver(version) !== void 0 ? version : null;
657
+ };
658
+ }
456
659
  export {
457
660
  PluginUpdateError,
458
- createPluginUpdater
661
+ createPluginUpdater,
662
+ createPnpmViewRunner
459
663
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morehao/dsh-plugin-kit",
3
- "version": "0.1.0",
4
- "description": "DeepSeek Harness 插件共用工具包:插件自更新引擎(查 npm 最新版 + 经宿主 pluginManager 重装)、headless 客户端控制器与 HTTP 小工具。零 @deepseek-ai 运行时依赖,宿主能力全部注入。",
3
+ "version": "0.3.0",
4
+ "description": "DeepSeek Harness 插件共用工具包:插件自更新引擎(查 npm 最新版 + 经宿主 pluginManager 重装)、headless 客户端控制器、HTTP 小工具,以及子进程执行 / 原子写 / JSON 状态持久化等通用底座。零 @deepseek-ai 运行时依赖,宿主能力全部注入。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -22,6 +22,18 @@
22
22
  "types": "./dist/http.d.ts",
23
23
  "default": "./dist/http.js"
24
24
  },
25
+ "./exec": {
26
+ "types": "./dist/exec.d.ts",
27
+ "default": "./dist/exec.js"
28
+ },
29
+ "./fs": {
30
+ "types": "./dist/fs.d.ts",
31
+ "default": "./dist/fs.js"
32
+ },
33
+ "./state": {
34
+ "types": "./dist/state.d.ts",
35
+ "default": "./dist/state.js"
36
+ },
25
37
  "./package.json": "./package.json"
26
38
  },
27
39
  "files": [
@@ -45,10 +57,13 @@
45
57
  "typecheck": "tsc --noEmit",
46
58
  "test": "node --test \"test/*.test.mjs\"",
47
59
  "pretest": "npm run build",
48
- "check": "npm run typecheck && npm run build && npm test --ignore-scripts"
60
+ "check": "npm run typecheck && npm run build && npm test --ignore-scripts",
61
+ "check:release-notes": "node scripts/smoke-release-notes.mjs",
62
+ "prepack": "npm run build"
49
63
  },
50
64
  "devDependencies": {
51
65
  "@types/node": "22.20.5",
66
+ "@types/semver": "^7.8.0",
52
67
  "tsup": "8.5.1",
53
68
  "typescript": "5.9.3"
54
69
  },
@@ -65,5 +80,8 @@
65
80
  "publishConfig": {
66
81
  "access": "public",
67
82
  "registry": "https://registry.npmjs.org/"
83
+ },
84
+ "dependencies": {
85
+ "semver": "^7.8.5"
68
86
  }
69
87
  }
@@ -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 };