@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/README.md +89 -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/index.d.ts +4 -32
- package/dist/index.js +45 -44
- package/dist/profile-RKpxKDlK.d.ts +117 -0
- package/dist/state.d.ts +32 -0
- package/dist/state.js +186 -0
- package/dist/update-client.d.ts +2 -1
- package/dist/update.d.ts +56 -5
- package/dist/update.js +244 -40
- package/package.json +21 -3
- package/dist/profile-C-eyP5F6.d.ts +0 -44
|
@@ -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 };
|
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,5 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ExecOptions, ExecResult } from './exec.js';
|
|
2
|
+
import { I as InstallKind, H as HostRegistryPlan } from './profile-RKpxKDlK.js';
|
|
3
|
+
export { P as ProfileLike } from './profile-RKpxKDlK.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 查 npm 上某个包的最新版本。
|
|
7
|
+
*
|
|
8
|
+
* 顺序(可配置):
|
|
9
|
+
* 1. 调用方注入的 `runPnpmView`(让插件用自己 profile 的包管理器查,继承 .npmrc 的
|
|
10
|
+
* 镜像 / 私源 / token;宿主内裸 fetch 读不到这些配置)。**这是复用宿主包管理器**:
|
|
11
|
+
* 查询在 profile 目录、profile 的 packageManager 环境下执行,与宿主安装同源。
|
|
12
|
+
* 2. 宿主 `pluginManager.registries()` 给出的 registry 列表,依次问 `<base><pkg>/latest`;
|
|
13
|
+
* 排序**优先用调用方注入的宿主原生 `registryPlan`**(与安装完全一致),未注入则内置兜底
|
|
14
|
+
* (见 `registry.ts` 的 `resolveRegistryPlan`);
|
|
15
|
+
* 3. 都没有就用官方 registry。
|
|
16
|
+
*
|
|
17
|
+
* **为什么不用宿主的 `pluginManager.inspect()` 查最新版**:`inspect()` 在 registry 分支会先判
|
|
18
|
+
* 「该包是否已在 profile 依赖中」,命中即返回 `already-installed`,不会真正去问 registry。
|
|
19
|
+
* 插件自更新要查的正是「我自己这个已装包」,必然命中该拦截,所以此处只能走 `pnpm view` / `/latest`。
|
|
20
|
+
*
|
|
21
|
+
* 全部失败返回 null(不抛):调用方用它标记 `checkFailed`,界面照常可用。注意「查不到」≠「宿主缺失」,
|
|
22
|
+
* 而是网络 / registry / 包管理器任一环节不可用时的运行时降级。
|
|
23
|
+
*/
|
|
3
24
|
|
|
4
25
|
type Logger = (event: string, detail?: Record<string, unknown>) => void;
|
|
5
26
|
interface PluginMeta {
|
|
@@ -13,13 +34,19 @@ interface PluginMeta {
|
|
|
13
34
|
*
|
|
14
35
|
* 设计约束(三个 DSH 插件共用这份逻辑):
|
|
15
36
|
* - **零 `@deepseek-ai/*` 运行时依赖**:pluginManager / profileContext 都由调用方用
|
|
16
|
-
* `ctx.get(...)`
|
|
37
|
+
* `ctx.get(...)` 惰性取。本包定位就是 DSH 插件,宿主必然在;但 `installBundle` 是
|
|
38
|
+
* 可选/远程能力,取不到只是「当前部署未暴露该能力」,退化成「只检查 + 手动命令」。
|
|
39
|
+
* - **复用优先**:registry 顺序优先用调用方注入的宿主原生 `registryPlan`(与宿主安装完全一致,
|
|
40
|
+
* 含私有源语义),未注入则内置兜底(见 `registry.ts` 的 `resolveRegistryPlan`);查最新版走
|
|
41
|
+
* `pnpm view` / registry `/latest`(`inspect()` 对已装包返回 already-installed,不可替代)。
|
|
17
42
|
* - **不自作主张升级**:只有 registry 明确给出合法且严格更新的版本才允许安装;
|
|
18
43
|
* `link:` / `file:` / `workspace:` 安装一律禁止一键更新(会把开发链接换成 npm 版)。
|
|
19
44
|
* - **装不上要能回退**:宿主 `installBundle` 对已装包会返回 `already-installed`,按官方
|
|
20
45
|
* 更新路径先 remove 再 install;remove 之后 install 失败则尝试装回原版本。
|
|
21
46
|
* - **不重写宿主进程策略**:需要 `pnpm view` 时由调用方注入 `runPnpmView`(进程/环境
|
|
22
|
-
* 白名单是各插件自己的事),本包只做 registry
|
|
47
|
+
* 白名单是各插件自己的事),本包只做 registry 兜底。为免三个插件各抄一遍「读 profile → 拼
|
|
48
|
+
* `pnpm view` → 解析 → 校验」,本模块导出 `createPnpmViewRunner` 供直接复用(内部走 `./exec`,
|
|
49
|
+
* 环境清洗仍由调用方把握;也可注入 `exec` 替换进程策略)。
|
|
23
50
|
* - 更新会把运行中的插件自己卸载,界面会一起消失:全过程落固定事件名,便于事后对账。
|
|
24
51
|
*/
|
|
25
52
|
|
|
@@ -63,6 +90,8 @@ interface UpdaterDeps {
|
|
|
63
90
|
getProfileName?: () => string | null;
|
|
64
91
|
fetchImpl?: typeof fetch;
|
|
65
92
|
runPnpmView?: (spec: string) => Promise<string | null>;
|
|
93
|
+
/** 可选:注入宿主原生 `registryPlan`(dsh-plugin-manager/registry),让查版本的 registry 顺序与安装一致。 */
|
|
94
|
+
registryPlan?: HostRegistryPlan;
|
|
66
95
|
readFile?: (path: string) => Promise<string>;
|
|
67
96
|
logger?: Logger;
|
|
68
97
|
now?: () => number;
|
|
@@ -83,5 +112,27 @@ interface Updater {
|
|
|
83
112
|
isApplying(): boolean;
|
|
84
113
|
}
|
|
85
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>;
|
|
86
137
|
|
|
87
|
-
export { type ApplyResult, 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 };
|