@morehao/dsh-plugin-kit 0.2.1 → 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/README.md +71 -4
- package/dist/exec.d.ts +52 -0
- package/dist/exec.js +171 -0
- package/dist/fs.d.ts +12 -0
- package/dist/fs.js +119 -0
- package/dist/state.d.ts +32 -0
- package/dist/state.js +186 -0
- package/dist/update-client.d.ts +1 -0
- package/dist/update.d.ts +27 -2
- package/dist/update.js +199 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# @morehao/dsh-plugin-kit
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness(dsh
|
|
3
|
+
DeepSeek Harness(dsh)插件共用的工具包。当前提供这些能力:
|
|
4
4
|
|
|
5
5
|
- **插件自更新引擎**(`./update`):查 npm 最新版 → 判断能不能一键更新 → 经宿主 `pluginManager` 重装自己。
|
|
6
6
|
- **headless 客户端控制器**(`./update/client`):检查/更新的状态机与单飞,UI 由各插件自己渲染。
|
|
7
7
|
- **HTTP 小工具**(`./http`):自定义头鉴权、`no-store` JSON、有上限的 JSON body。
|
|
8
|
+
- **子进程执行**(`./exec`):argv 直传(不经 shell)、环境白名单、超时/取消、输出上限,外加 `pnpm view` 版本解析。
|
|
9
|
+
- **原子写**(`./fs`):`writeFileAtomicSync` / `writeFileAtomic`,杜绝「半个文件」。
|
|
10
|
+
- **JSON 状态持久化**(`./state`):`$DSH_HOME` 解析 + `<home>/<plugin>/*.json` 读写(读 fail-open、写抛错)。
|
|
8
11
|
|
|
9
|
-
设计约束:**零 `@deepseek-ai/*` 运行时依赖**,宿主能力(`pluginManager` / `profileContext
|
|
12
|
+
设计约束:**零 `@deepseek-ai/*` 运行时依赖**,宿主能力(`pluginManager` / `profileContext`)全部由调用方注入。进程策略由调用方把握:`./update` 常驻能力不主动 spawn,需要 `pnpm view` 时由调用方注入 `runPnpmView`;也可直接用 `./exec`(或 `createPnpmViewRunner`,默认走 `./exec`、可注入 `exec` 替换进程策略)。
|
|
10
13
|
|
|
11
14
|
## 安装
|
|
12
15
|
|
|
@@ -114,15 +117,79 @@ const body = await readJsonBody(request, { maxBytes: 512 * 1024 });
|
|
|
114
117
|
sendJson(response, 200, { ok: true });
|
|
115
118
|
```
|
|
116
119
|
|
|
120
|
+
## 子进程执行(`./exec`)
|
|
121
|
+
|
|
122
|
+
argv 直传、不经 shell;超时/取消先 SIGTERM、宽限后 SIGKILL;输出有上限且保留尾部;`spawn` 失败**不 reject**,落到 `error` 字段。
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { spawnCollect, sanitizedEnv, collectVersionView } from '@morehao/dsh-plugin-kit/exec';
|
|
126
|
+
|
|
127
|
+
const result = await spawnCollect('git', ['rev-parse', 'HEAD'], {
|
|
128
|
+
cwd: repo,
|
|
129
|
+
env: sanitizedEnv(process.env), // 只放行 PATH/HOME/代理/证书/locale/git 身份等白名单
|
|
130
|
+
timeoutMs: 8_000,
|
|
131
|
+
});
|
|
132
|
+
// { code, signal, stdout, stderr, output, timedOut, aborted, durationMs, error? }
|
|
133
|
+
|
|
134
|
+
// 从 `pnpm view <spec> version --json` 的输出里取版本串(单值或数组取尾;非法 → null)
|
|
135
|
+
const version = collectVersionView({ code: result.code, stdout: result.stdout });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 原子写(`./fs`)
|
|
139
|
+
|
|
140
|
+
同目录临时文件 → fsync → rename → fsync(父目录);Windows rename 有界重试;失败清理临时文件,原文件在 rename 前不被截断。
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { writeFileAtomicSync, writeFileAtomic } from '@morehao/dsh-plugin-kit/fs';
|
|
144
|
+
|
|
145
|
+
writeFileAtomicSync('/path/state.json', `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
146
|
+
await writeFileAtomic('/path/state.json', content); // 同语义异步版
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`mode` 省略时沿用已有文件权限,新文件走 `defaultMode`(默认 `0o644`)。
|
|
150
|
+
|
|
151
|
+
## JSON 状态持久化(`./state`)
|
|
152
|
+
|
|
153
|
+
`$DSH_HOME` 解析 + `<home>/<plugin>/*.json` 读写。**读 fail-open**(缺失/坏 JSON/非对象 → `fallback()`,永不抛);**写抛错**(写通常是一次事务,静默吞掉会掩盖数据丢失,需要「尽力而为」的调用方自行 try/catch)。
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { resolveDshHome, createJsonStore } from '@morehao/dsh-plugin-kit/state';
|
|
157
|
+
|
|
158
|
+
const store = createJsonStore({
|
|
159
|
+
dir: `${resolveDshHome()}/my-plugin`, // $DSH_HOME → ~/.dsh
|
|
160
|
+
file: 'state.json',
|
|
161
|
+
fallback: () => ({ lastSeenVersion: '' }), // 工厂函数,避免共享可变引用
|
|
162
|
+
});
|
|
163
|
+
const state = store.read(); // 坏文件也返回 fallback
|
|
164
|
+
store.write({ lastSeenVersion: '0.3.0' }); // 原子写
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 复用 `pnpm view` 查版本(`./update`)
|
|
168
|
+
|
|
169
|
+
三仓此前各手写一遍的「读 profile → 拼 `pnpm view` → 解析 → 校验」,收敛为一个工厂:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { createPnpmViewRunner, createPluginUpdater } from '@morehao/dsh-plugin-kit/update';
|
|
173
|
+
|
|
174
|
+
const runPnpmView = createPnpmViewRunner({
|
|
175
|
+
getProfile: () => ctx.get('profileContext'),
|
|
176
|
+
// 可选:注入自定义 exec 以沿用本插件更细的进程策略;默认走 ./exec
|
|
177
|
+
// exec: myExecCapture,
|
|
178
|
+
});
|
|
179
|
+
const updater = createPluginUpdater({ meta, getPluginManager: () => ctx.get('pluginManager'), runPnpmView });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
拿不到 `profile.dir` / 命令失败 / 输出非法 / 版本不是合法 semver → 返回 `null`(交由 registry 兜底)。
|
|
183
|
+
|
|
117
184
|
## 版本与兼容
|
|
118
185
|
|
|
119
186
|
- Node `^22.20.0 || >=24.0.0`,纯 ESM。
|
|
120
|
-
- `./update
|
|
187
|
+
- `./update`、`./http`、`./exec`、`./fs`、`./state` 走 host 侧;`./update/client` 只含状态机,不 import node 内置模块(客户端产物只允许 react)。
|
|
121
188
|
- 宿主服务形状按结构化类型处理(`PluginManagerLike` / `ProfileLike`),宿主版本变化不会把插件一起拖下去。
|
|
122
189
|
|
|
123
190
|
## 开发
|
|
124
191
|
|
|
125
192
|
```bash
|
|
126
193
|
npm install
|
|
127
|
-
npm run check # typecheck + build +
|
|
194
|
+
npm run check # typecheck + build + 49 条离线测试(假 fetch / 假 pluginManager / 假 exec,不联网、不真装)
|
|
128
195
|
```
|
package/dist/exec.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface ExecResult {
|
|
2
|
+
code: number | null;
|
|
3
|
+
signal: NodeJS.Signals | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
/** stdout + stderr 交错合并后的结果(同 dsh-commands 的 `output`)。 */
|
|
7
|
+
output: string;
|
|
8
|
+
timedOut: boolean;
|
|
9
|
+
aborted: boolean;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
/** spawn 本身失败(ENOENT / EACCES 等)时带出;此时 `code === null` 且不 reject。 */
|
|
12
|
+
error?: Error;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 从 `process.env` 里挑出白名单键 + 调用方额外给的键,拼成子进程环境。
|
|
16
|
+
* 空字符串视为未设置(避免把 `KEY=` 传给子进程,部分工具会当成空值处理)。
|
|
17
|
+
*/
|
|
18
|
+
declare function sanitizedEnv(source?: NodeJS.ProcessEnv, extra?: Record<string, string | undefined>): Record<string, string>;
|
|
19
|
+
interface ExecOptions {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** 省略时继承 `process.env`;要清洗时先过 `sanitizedEnv` 再传进来。 */
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** 超时毫秒;默认 10_000。传 0 / Infinity / 非正数表示不设超时。 */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** SIGTERM 之后到 SIGKILL 的宽限;默认 2_000。 */
|
|
27
|
+
abortGraceMs?: number;
|
|
28
|
+
/** stdout 上限(保留尾部);默认 1 MiB。 */
|
|
29
|
+
maxStdout?: number;
|
|
30
|
+
/** stderr 上限(保留尾部);默认 1 MiB。 */
|
|
31
|
+
maxStderr?: number;
|
|
32
|
+
/** 合并 `output` 的上限(保留尾部);默认 = maxStdout + maxStderr。 */
|
|
33
|
+
maxOutput?: number;
|
|
34
|
+
/** 写入子进程 stdin(写完即关闭);省略则 stdin = 'ignore'。 */
|
|
35
|
+
input?: string;
|
|
36
|
+
/** 原始分片旁路(去 ANSI 由调用方决定);回调抛错会被吞,不影响采集。 */
|
|
37
|
+
onData?: (text: string, channel: 'stdout' | 'stderr') => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 执行一次命令并收集输出。永不 reject:进程级失败落到 `error` 字段。
|
|
41
|
+
*/
|
|
42
|
+
declare function spawnCollect(command: string, args?: string[], options?: ExecOptions): Promise<ExecResult>;
|
|
43
|
+
/**
|
|
44
|
+
* 从 `pnpm view <spec> version --json` 的 stdout 取版本字符串。
|
|
45
|
+
* - 成功:可能返回单值或数组(多版本时 pnpm 给数组),取最后一个非空字符串;
|
|
46
|
+
* - 空输出 / 非 JSON / 取不到字符串 → null。
|
|
47
|
+
*
|
|
48
|
+
* **不在这里做 semver 合法性校验**(避免 `./exec` 依赖 `./semver`),由调用方用 `parseSemver` 判定。
|
|
49
|
+
*/
|
|
50
|
+
declare function collectVersionView(result: Pick<ExecResult, 'code' | 'stdout'>): string | null;
|
|
51
|
+
|
|
52
|
+
export { type ExecOptions, type ExecResult, collectVersionView, sanitizedEnv, spawnCollect };
|
package/dist/exec.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
export {
|
|
168
|
+
collectVersionView,
|
|
169
|
+
sanitizedEnv,
|
|
170
|
+
spawnCollect
|
|
171
|
+
};
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface WriteFileAtomicOptions {
|
|
2
|
+
/** 目标权限位;省略时沿用已有文件权限,新文件用 `defaultMode`。 */
|
|
3
|
+
mode?: number;
|
|
4
|
+
/** 新文件默认权限位;已有文件仍沿用其原权限。默认 0o644。 */
|
|
5
|
+
defaultMode?: number;
|
|
6
|
+
}
|
|
7
|
+
/** 同步原子整文件替换(`mkdir → open('wx') → write → fsync → rename → fsync(dir)`)。 */
|
|
8
|
+
declare function writeFileAtomicSync(file: string, content: string, options?: WriteFileAtomicOptions): void;
|
|
9
|
+
/** 异步原子整文件替换,语义与同步版一致(`fs/promises`)。 */
|
|
10
|
+
declare function writeFileAtomic(file: string, content: string, options?: WriteFileAtomicOptions): Promise<void>;
|
|
11
|
+
|
|
12
|
+
export { type WriteFileAtomicOptions, writeFileAtomic, writeFileAtomicSync };
|
package/dist/fs.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/fs.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import fsp from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
6
|
+
var WINDOWS_TRANSIENT_RENAME = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
7
|
+
var RENAME_RETRY_DELAYS_MS = [20, 40, 80, 160, 200, 200, 200, 200];
|
|
8
|
+
function resolveMode(file, options) {
|
|
9
|
+
if (typeof options.mode === "number") return options.mode;
|
|
10
|
+
try {
|
|
11
|
+
return fs.statSync(file).mode & 511;
|
|
12
|
+
} catch {
|
|
13
|
+
return options.defaultMode ?? 420;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function tempPath(dir, base) {
|
|
17
|
+
return path.join(dir, `.${base}.${process.pid}.${Date.now()}.${randomBytes(4).toString("hex")}.tmp`);
|
|
18
|
+
}
|
|
19
|
+
function sleepSync(ms) {
|
|
20
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
21
|
+
}
|
|
22
|
+
function renameWithRetrySync(from, to) {
|
|
23
|
+
if (process.platform !== "win32") {
|
|
24
|
+
fs.renameSync(from, to);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
28
|
+
try {
|
|
29
|
+
fs.renameSync(from, to);
|
|
30
|
+
return;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const code = error?.code;
|
|
33
|
+
if (code === void 0 || !WINDOWS_TRANSIENT_RENAME.has(code) || attempt >= RENAME_RETRY_DELAYS_MS.length) throw error;
|
|
34
|
+
sleepSync(RENAME_RETRY_DELAYS_MS[attempt]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function fsyncDirSync(dir) {
|
|
39
|
+
if (process.platform === "win32") return;
|
|
40
|
+
try {
|
|
41
|
+
const dirFd = fs.openSync(dir, "r");
|
|
42
|
+
try {
|
|
43
|
+
fs.fsyncSync(dirFd);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.closeSync(dirFd);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeFileAtomicSync(file, content, options = {}) {
|
|
51
|
+
const dir = path.dirname(file);
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
const mode = resolveMode(file, options);
|
|
54
|
+
const tmp = tempPath(dir, path.basename(file));
|
|
55
|
+
let fd = null;
|
|
56
|
+
try {
|
|
57
|
+
fd = fs.openSync(tmp, "wx", mode);
|
|
58
|
+
fs.writeFileSync(fd, content, "utf8");
|
|
59
|
+
fs.fsyncSync(fd);
|
|
60
|
+
fs.closeSync(fd);
|
|
61
|
+
fd = null;
|
|
62
|
+
renameWithRetrySync(tmp, file);
|
|
63
|
+
fsyncDirSync(dir);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (fd !== null) {
|
|
66
|
+
try {
|
|
67
|
+
fs.closeSync(fd);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(tmp, { force: true });
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function writeFileAtomic(file, content, options = {}) {
|
|
79
|
+
const dir = path.dirname(file);
|
|
80
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
81
|
+
const mode = resolveMode(file, options);
|
|
82
|
+
const tmp = tempPath(dir, path.basename(file));
|
|
83
|
+
let handle = null;
|
|
84
|
+
try {
|
|
85
|
+
handle = await fsp.open(tmp, "wx", mode);
|
|
86
|
+
await handle.writeFile(content, "utf8");
|
|
87
|
+
await handle.sync();
|
|
88
|
+
await handle.close();
|
|
89
|
+
handle = null;
|
|
90
|
+
await fsp.rename(tmp, file);
|
|
91
|
+
if (process.platform !== "win32") {
|
|
92
|
+
try {
|
|
93
|
+
const dirFd = await fsp.open(dir, "r");
|
|
94
|
+
try {
|
|
95
|
+
await dirFd.sync();
|
|
96
|
+
} finally {
|
|
97
|
+
await dirFd.close();
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (handle !== null) {
|
|
104
|
+
try {
|
|
105
|
+
await handle.close();
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await fsp.rm(tmp, { force: true });
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
writeFileAtomic,
|
|
118
|
+
writeFileAtomicSync
|
|
119
|
+
};
|
package/dist/state.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/update-client.d.ts
CHANGED
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.
|
|
4
|
-
"description": "DeepSeek Harness 插件共用工具包:插件自更新引擎(查 npm 最新版 + 经宿主 pluginManager 重装)、headless
|
|
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,7 +57,9 @@
|
|
|
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",
|