@morehao/dsh-plugin-kit 0.2.1 → 0.3.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/dist/http.js CHANGED
@@ -43,8 +43,48 @@ async function readJsonBody(request, options = {}) {
43
43
  }
44
44
  return value;
45
45
  }
46
+ function createRouteHandler(options) {
47
+ const maxBodyBytes = options.maxBodyBytes ?? 512 * 1024;
48
+ const logger = options.logger ?? (() => {
49
+ });
50
+ const prefix = options.prefix;
51
+ return async (rawRequest, rawResponse) => {
52
+ const request = rawRequest;
53
+ const response = rawResponse;
54
+ if (!hasHeader(request, options.header)) {
55
+ sendJson(response, 403, { ok: false, code: "FORBIDDEN", message: `\u7F3A\u5C11 ${options.header}: 1 \u5934` });
56
+ return;
57
+ }
58
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
59
+ const pathname = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) || "/" : url.pathname;
60
+ const method = (request.method ?? "GET").toUpperCase();
61
+ try {
62
+ const body = method === "GET" || method === "HEAD" ? {} : await readJsonBody(request, { maxBytes: maxBodyBytes });
63
+ const result = await options.dispatch({ request, url, method, pathname, body });
64
+ if (response.writableEnded !== true) {
65
+ sendJson(response, 200, result === void 0 ? { ok: true } : { ok: true, ...result });
66
+ }
67
+ } catch (error) {
68
+ if (response.writableEnded === true) return;
69
+ const mapped = options.onError?.(error);
70
+ if (mapped !== void 0) {
71
+ sendJson(response, mapped.status, {
72
+ ok: false,
73
+ ...mapped.code === void 0 ? {} : { code: mapped.code },
74
+ message: mapped.message,
75
+ ...mapped.extra === void 0 ? {} : mapped.extra
76
+ });
77
+ return;
78
+ }
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ logger("route.error", { method, pathname, message });
81
+ sendJson(response, 500, { ok: false, code: "INTERNAL", message });
82
+ }
83
+ };
84
+ }
46
85
  export {
47
86
  HttpError,
87
+ createRouteHandler,
48
88
  hasHeader,
49
89
  readJsonBody,
50
90
  sendJson
package/dist/meta.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ interface PluginMetaInfo {
2
+ name: string;
3
+ version: string;
4
+ /** 规范化后的 `https://github.com/owner/repo`;解析不了则缺省。 */
5
+ repository?: string;
6
+ /** `owner/repo` 短名;解析不了为 null。 */
7
+ repo: string | null;
8
+ }
9
+ interface ReadFileSyncLike {
10
+ (path: string | URL): string;
11
+ }
12
+ interface CreatePluginMetaOptions {
13
+ /** `new URL('../package.json', import.meta.url)` 这类导入地址。 */
14
+ pkgUrl: URL | string;
15
+ defaultName?: string;
16
+ /** 注入读文件(测试/奇特环境);默认 `node:fs` readFileSync。 */
17
+ readFileSync?: ReadFileSyncLike;
18
+ }
19
+ /** 读一次 package.json,产出插件身份;读失败用 defaultName + `0.0.0` 兜底。 */
20
+ declare function createPluginMeta(options: CreatePluginMetaOptions): PluginMetaInfo;
21
+ interface ReadInstalledVersionOptions {
22
+ pkgUrl: URL | string;
23
+ readFileSync?: ReadFileSyncLike;
24
+ }
25
+ /** 每次重读 package.json 的 version(更新插件后同进程能读到新值);失败 → ''。 */
26
+ declare function readInstalledVersion(options: ReadInstalledVersionOptions): string;
27
+
28
+ export { type CreatePluginMetaOptions, type PluginMetaInfo, type ReadFileSyncLike, type ReadInstalledVersionOptions, createPluginMeta, readInstalledVersion };
package/dist/meta.js ADDED
@@ -0,0 +1,82 @@
1
+ // src/meta.ts
2
+ import { readFileSync as fsReadFileSync } from "fs";
3
+
4
+ // src/semver.ts
5
+ import semver from "semver";
6
+
7
+ // src/github.ts
8
+ var GITHUB_HOST = "github.com";
9
+ var DEFAULT_TTL_MS = 10 * 60 * 1e3;
10
+ function normalizeRepositoryUrl(value) {
11
+ if (typeof value !== "string") return void 0;
12
+ let url = value.trim();
13
+ if (url === "") return void 0;
14
+ url = url.replace(/^git\+/i, "");
15
+ const ssh = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/i.exec(url);
16
+ if (ssh !== null) url = `https://github.com/${ssh[1]}/${ssh[2]}`;
17
+ url = url.replace(/\.git$/i, "");
18
+ try {
19
+ const parsed = new URL(url);
20
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
21
+ if (parsed.hostname.toLowerCase() !== GITHUB_HOST) return void 0;
22
+ const pathname = parsed.pathname.replace(/\/+$/, "");
23
+ if (pathname === "") return void 0;
24
+ return `https://github.com${pathname}`;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ function repoSlug(value) {
30
+ if (typeof value !== "string") return null;
31
+ const text = value.trim();
32
+ if (text === "") return null;
33
+ if (/^[\w.-]+\/[\w.-]+$/.test(text)) return text;
34
+ const normalized = normalizeRepositoryUrl(text);
35
+ if (normalized === void 0) return null;
36
+ const pathname = normalized.slice(`https://github.com/`.length);
37
+ return pathname === "" ? null : pathname;
38
+ }
39
+
40
+ // src/meta.ts
41
+ function defaultReadFileSync(path) {
42
+ return fsReadFileSync(path, "utf8");
43
+ }
44
+ function createPluginMeta(options) {
45
+ const readFileSync = options.readFileSync ?? defaultReadFileSync;
46
+ const fallbackName = options.defaultName ?? "";
47
+ try {
48
+ const parsed = JSON.parse(readFileSync(options.pkgUrl));
49
+ if (parsed === null || typeof parsed !== "object") {
50
+ return { name: fallbackName, version: "0.0.0", repo: null };
51
+ }
52
+ const pkg = parsed;
53
+ const rawRepo = typeof pkg.repository === "string" ? pkg.repository : pkg.repository !== null && typeof pkg.repository === "object" ? pkg.repository.url : void 0;
54
+ const repository = normalizeRepositoryUrl(rawRepo);
55
+ const repo = repoSlug(repository);
56
+ return {
57
+ name: typeof pkg.name === "string" && pkg.name !== "" ? pkg.name : fallbackName,
58
+ version: typeof pkg.version === "string" && pkg.version !== "" ? pkg.version : "0.0.0",
59
+ ...repository === void 0 ? {} : { repository },
60
+ repo
61
+ };
62
+ } catch {
63
+ return { name: fallbackName, version: "0.0.0", repo: null };
64
+ }
65
+ }
66
+ function readInstalledVersion(options) {
67
+ const readFileSync = options.readFileSync ?? defaultReadFileSync;
68
+ try {
69
+ const parsed = JSON.parse(readFileSync(options.pkgUrl));
70
+ if (parsed !== null && typeof parsed === "object") {
71
+ const version = parsed.version;
72
+ if (typeof version === "string") return version.trim();
73
+ }
74
+ return "";
75
+ } catch {
76
+ return "";
77
+ }
78
+ }
79
+ export {
80
+ createPluginMeta,
81
+ readInstalledVersion
82
+ };
@@ -0,0 +1,32 @@
1
+ /** `$DSH_HOME`(去空白、`path.resolve`)→ 缺省 `homedir()/.dsh`。与 dsh-core 及三仓既有实现一致。 */
2
+ declare function resolveDshHome(env?: NodeJS.ProcessEnv): string;
3
+ interface JsonStoreOptions<T> {
4
+ /** 状态目录(绝对路径)。调用方用 `resolveDshHome` + 插件目录名拼。 */
5
+ dir: string;
6
+ /** 文件名,如 `'state.json'`。 */
7
+ file: string;
8
+ /** 读失败 / 缺失 / 非对象时返回的默认值(工厂函数,避免共享可变引用)。 */
9
+ fallback: () => T;
10
+ /** 写回时的文件权限位;默认 0o600(状态文件默认私有)。 */
11
+ mode?: number;
12
+ /** mkdir 的目录权限;默认 0o700。 */
13
+ dirMode?: number;
14
+ /** 测试注入:默认读真实文件。 */
15
+ readFile?: (path: string) => Promise<string>;
16
+ readFileSync?: (path: string) => string;
17
+ }
18
+ interface JsonStore<T> {
19
+ /** 状态文件的绝对路径。 */
20
+ readonly path: string;
21
+ /** 同步读:坏文件 / 缺失 / 非对象 → `fallback()`。永不抛。 */
22
+ read(): T;
23
+ /** 同步原子写(`writeFileAtomicSync`)。失败抛错。 */
24
+ write(value: T): void;
25
+ /** 异步读。 */
26
+ readAsync(): Promise<T>;
27
+ /** 异步原子写(`writeFileAtomic`)。失败抛错。 */
28
+ writeAsync(value: T): Promise<void>;
29
+ }
30
+ declare function createJsonStore<T>(options: JsonStoreOptions<T>): JsonStore<T>;
31
+
32
+ export { type JsonStore, type JsonStoreOptions, createJsonStore, resolveDshHome };
package/dist/state.js ADDED
@@ -0,0 +1,186 @@
1
+ // src/state.ts
2
+ import { mkdirSync, promises as fsp2, readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+
6
+ // src/fs.ts
7
+ import fs from "fs";
8
+ import fsp from "fs/promises";
9
+ import path from "path";
10
+ import { randomBytes } from "crypto";
11
+ var WINDOWS_TRANSIENT_RENAME = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
12
+ var RENAME_RETRY_DELAYS_MS = [20, 40, 80, 160, 200, 200, 200, 200];
13
+ function resolveMode(file, options) {
14
+ if (typeof options.mode === "number") return options.mode;
15
+ try {
16
+ return fs.statSync(file).mode & 511;
17
+ } catch {
18
+ return options.defaultMode ?? 420;
19
+ }
20
+ }
21
+ function tempPath(dir, base) {
22
+ return path.join(dir, `.${base}.${process.pid}.${Date.now()}.${randomBytes(4).toString("hex")}.tmp`);
23
+ }
24
+ function sleepSync(ms) {
25
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
26
+ }
27
+ function renameWithRetrySync(from, to) {
28
+ if (process.platform !== "win32") {
29
+ fs.renameSync(from, to);
30
+ return;
31
+ }
32
+ for (let attempt = 0; ; attempt += 1) {
33
+ try {
34
+ fs.renameSync(from, to);
35
+ return;
36
+ } catch (error) {
37
+ const code = error?.code;
38
+ if (code === void 0 || !WINDOWS_TRANSIENT_RENAME.has(code) || attempt >= RENAME_RETRY_DELAYS_MS.length) throw error;
39
+ sleepSync(RENAME_RETRY_DELAYS_MS[attempt]);
40
+ }
41
+ }
42
+ }
43
+ function fsyncDirSync(dir) {
44
+ if (process.platform === "win32") return;
45
+ try {
46
+ const dirFd = fs.openSync(dir, "r");
47
+ try {
48
+ fs.fsyncSync(dirFd);
49
+ } finally {
50
+ fs.closeSync(dirFd);
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ function writeFileAtomicSync(file, content, options = {}) {
56
+ const dir = path.dirname(file);
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ const mode = resolveMode(file, options);
59
+ const tmp = tempPath(dir, path.basename(file));
60
+ let fd = null;
61
+ try {
62
+ fd = fs.openSync(tmp, "wx", mode);
63
+ fs.writeFileSync(fd, content, "utf8");
64
+ fs.fsyncSync(fd);
65
+ fs.closeSync(fd);
66
+ fd = null;
67
+ renameWithRetrySync(tmp, file);
68
+ fsyncDirSync(dir);
69
+ } catch (error) {
70
+ if (fd !== null) {
71
+ try {
72
+ fs.closeSync(fd);
73
+ } catch {
74
+ }
75
+ }
76
+ try {
77
+ fs.rmSync(tmp, { force: true });
78
+ } catch {
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+ async function writeFileAtomic(file, content, options = {}) {
84
+ const dir = path.dirname(file);
85
+ await fsp.mkdir(dir, { recursive: true });
86
+ const mode = resolveMode(file, options);
87
+ const tmp = tempPath(dir, path.basename(file));
88
+ let handle = null;
89
+ try {
90
+ handle = await fsp.open(tmp, "wx", mode);
91
+ await handle.writeFile(content, "utf8");
92
+ await handle.sync();
93
+ await handle.close();
94
+ handle = null;
95
+ await fsp.rename(tmp, file);
96
+ if (process.platform !== "win32") {
97
+ try {
98
+ const dirFd = await fsp.open(dir, "r");
99
+ try {
100
+ await dirFd.sync();
101
+ } finally {
102
+ await dirFd.close();
103
+ }
104
+ } catch {
105
+ }
106
+ }
107
+ } catch (error) {
108
+ if (handle !== null) {
109
+ try {
110
+ await handle.close();
111
+ } catch {
112
+ }
113
+ }
114
+ try {
115
+ await fsp.rm(tmp, { force: true });
116
+ } catch {
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ // src/state.ts
123
+ function resolveDshHome(env = process.env) {
124
+ const raw = env.DSH_HOME?.trim();
125
+ return raw !== void 0 && raw !== "" ? raw : join(homedir(), ".dsh");
126
+ }
127
+ function serialize(value) {
128
+ return `${JSON.stringify(value, null, 2)}
129
+ `;
130
+ }
131
+ function coerce(text, fallback) {
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(text);
135
+ } catch {
136
+ return fallback();
137
+ }
138
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return fallback();
139
+ return parsed;
140
+ }
141
+ var defaultReadFileSync = (target) => readFileSync(target, "utf8");
142
+ var defaultReadFile = (target) => fsp2.readFile(target, "utf8");
143
+ function createJsonStore(options) {
144
+ const file = join(options.dir, options.file);
145
+ const mode = options.mode ?? 384;
146
+ const dirMode = options.dirMode ?? 448;
147
+ const readFileSyncImpl = options.readFileSync ?? defaultReadFileSync;
148
+ const readFileImpl = options.readFile ?? defaultReadFile;
149
+ function read() {
150
+ let text;
151
+ try {
152
+ text = readFileSyncImpl(file);
153
+ } catch {
154
+ return options.fallback();
155
+ }
156
+ return coerce(text, options.fallback);
157
+ }
158
+ async function readAsync() {
159
+ let text;
160
+ try {
161
+ text = await readFileImpl(file);
162
+ } catch {
163
+ return options.fallback();
164
+ }
165
+ return coerce(text, options.fallback);
166
+ }
167
+ function write(value) {
168
+ try {
169
+ mkdirSync(options.dir, { recursive: true, mode: dirMode });
170
+ } catch {
171
+ }
172
+ writeFileAtomicSync(file, serialize(value), { mode });
173
+ }
174
+ async function writeAsync(value) {
175
+ try {
176
+ await fsp2.mkdir(options.dir, { recursive: true, mode: dirMode });
177
+ } catch {
178
+ }
179
+ await writeFileAtomic(file, serialize(value), { mode });
180
+ }
181
+ return { path: file, read, write, readAsync, writeAsync };
182
+ }
183
+ export {
184
+ createJsonStore,
185
+ resolveDshHome
186
+ };
@@ -1,4 +1,5 @@
1
1
  import { UpdateView, ApplyResult } from './update.js';
2
+ import './exec.js';
2
3
  import './profile-RKpxKDlK.js';
3
4
 
4
5
  /**
package/dist/update.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExecOptions, ExecResult } from './exec.js';
1
2
  import { I as InstallKind, H as HostRegistryPlan } from './profile-RKpxKDlK.js';
2
3
  export { P as ProfileLike } from './profile-RKpxKDlK.js';
3
4
 
@@ -43,7 +44,9 @@ interface PluginMeta {
43
44
  * - **装不上要能回退**:宿主 `installBundle` 对已装包会返回 `already-installed`,按官方
44
45
  * 更新路径先 remove 再 install;remove 之后 install 失败则尝试装回原版本。
45
46
  * - **不重写宿主进程策略**:需要 `pnpm view` 时由调用方注入 `runPnpmView`(进程/环境
46
- * 白名单是各插件自己的事),本包只做 registry 兜底。
47
+ * 白名单是各插件自己的事),本包只做 registry 兜底。为免三个插件各抄一遍「读 profile → 拼
48
+ * `pnpm view` → 解析 → 校验」,本模块导出 `createPnpmViewRunner` 供直接复用(内部走 `./exec`,
49
+ * 环境清洗仍由调用方把握;也可注入 `exec` 替换进程策略)。
47
50
  * - 更新会把运行中的插件自己卸载,界面会一起消失:全过程落固定事件名,便于事后对账。
48
51
  */
49
52
 
@@ -109,5 +112,27 @@ interface Updater {
109
112
  isApplying(): boolean;
110
113
  }
111
114
  declare function createPluginUpdater(deps: UpdaterDeps): Updater;
115
+ interface PnpmViewRunnerDeps {
116
+ /** 取宿主 `profileContext`(提供 `dir` / `packageManager`);可抛,内部会兜。 */
117
+ getProfile: () => unknown;
118
+ /**
119
+ * 可注入的子进程执行器(默认 `./exec` 的 `spawnCollect`)。
120
+ * 各插件若要沿用自己更细的进程策略(环境白名单 / 输出上限 / 作业面板旁路),注入即可。
121
+ */
122
+ exec?: (command: string, args: string[], options?: ExecOptions) => Promise<ExecResult>;
123
+ /** 环境来源;默认 `process.env`,内部过 `sanitizedEnv` 清洗。 */
124
+ env?: NodeJS.ProcessEnv;
125
+ /** 单个查询超时毫秒;默认 8_000。 */
126
+ timeoutMs?: number;
127
+ }
128
+ /**
129
+ * 造一个 `runPnpmView`:用 profile 的包管理器在 profile 目录执行
130
+ * `pnpm view <spec> version --json`,继承 `.npmrc` 的镜像 / 私源 / token。
131
+ *
132
+ * 返回 `(spec) => Promise<string | null>`,与 `UpdaterDeps.runPnpmView` 兼容:
133
+ * 拿不到 profile.dir / 命令失败 / 输出非法 / 版本不是合法 semver → `null`(交由 registry 兜底)。
134
+ * 三个插件此前各自手写这段,本工厂与它们的历史行为一致。
135
+ */
136
+ declare function createPnpmViewRunner(deps: PnpmViewRunnerDeps): (spec: string) => Promise<string | null>;
112
137
 
113
- export { type ApplyResult, HostRegistryPlan, InstallKind, type Logger, type PluginManagerLike, type PluginMeta, PluginUpdateError, type UpdateErrorCode, type UpdateView, type Updater, type UpdaterDeps, createPluginUpdater };
138
+ export { type ApplyResult, HostRegistryPlan, InstallKind, type Logger, type PluginManagerLike, type PluginMeta, PluginUpdateError, type PnpmViewRunnerDeps, type UpdateErrorCode, type UpdateView, type Updater, type UpdaterDeps, createPluginUpdater, createPnpmViewRunner };
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;
@@ -459,7 +626,38 @@ function createPluginUpdater(deps) {
459
626
  isApplying: () => applying
460
627
  };
461
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
+ }
462
659
  export {
463
660
  PluginUpdateError,
464
- createPluginUpdater
661
+ createPluginUpdater,
662
+ createPnpmViewRunner
465
663
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morehao/dsh-plugin-kit",
3
- "version": "0.2.1",
4
- "description": "DeepSeek Harness 插件共用工具包:插件自更新引擎(查 npm 最新版 + 经宿主 pluginManager 重装)、headless 客户端控制器与 HTTP 小工具。零 @deepseek-ai 运行时依赖,宿主能力全部注入。",
3
+ "version": "0.3.1",
4
+ "description": "DeepSeek Harness 插件共用工具包:插件自更新引擎、headless 客户端控制器、HTTP 小工具与路由 dispatcher、子进程执行、原子写、JSON 状态持久化,以及 GitHub Release 读取与插件身份元数据等通用底座。零 @deepseek-ai 运行时依赖,宿主能力全部注入。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -22,6 +22,26 @@
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
+ },
37
+ "./github": {
38
+ "types": "./dist/github.d.ts",
39
+ "default": "./dist/github.js"
40
+ },
41
+ "./meta": {
42
+ "types": "./dist/meta.d.ts",
43
+ "default": "./dist/meta.js"
44
+ },
25
45
  "./package.json": "./package.json"
26
46
  },
27
47
  "files": [
@@ -45,7 +65,9 @@
45
65
  "typecheck": "tsc --noEmit",
46
66
  "test": "node --test \"test/*.test.mjs\"",
47
67
  "pretest": "npm run build",
48
- "check": "npm run typecheck && npm run build && npm test --ignore-scripts"
68
+ "check": "npm run typecheck && npm run build && npm test --ignore-scripts",
69
+ "check:release-notes": "node scripts/smoke-release-notes.mjs",
70
+ "prepack": "npm run build"
49
71
  },
50
72
  "devDependencies": {
51
73
  "@types/node": "22.20.5",