dsh-plugin-resolver 0.1.0 → 0.1.2
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/ABLATION.md +1 -1
- package/BENCHMARK.md +1 -1
- package/README.md +30 -16
- package/logic/api.js +33 -10
- package/logic/install.js +134 -22
- package/package.json +1 -1
package/ABLATION.md
CHANGED
package/BENCHMARK.md
CHANGED
|
@@ -71,6 +71,6 @@ node test/benchmark.mjs
|
|
|
71
71
|
## 4. 插件不能压缩的部分(诚实边界)
|
|
72
72
|
|
|
73
73
|
- **pnpm 下载与编译时间**是物理成本:镜像安装仍要 ~13 s,源码构建仍要 ~2 分钟。插件省的是决策、试错、验证与上下文,不是 CPU/带宽。
|
|
74
|
-
- **替换已加载插件**仍要重启宿主(`restart-required`):这是 loader 模块缓存 +
|
|
74
|
+
- **替换已加载插件**仍要重启宿主(`restart-required`):这是 loader 模块缓存 + 打包宿主没有 Node 内部 loader 的硬边界,插件如实报出来,不伪装成功。
|
|
75
75
|
- **同名歧义**仍需要一次人类/上层的选择(`owner=`),插件故意不猜。
|
|
76
76
|
- 目录是社区维护的第三方列表,插件只保证"按声明来源装对版本",不做安全审计。
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://github.com/sakikoTGW/dsh-plugin-resolver/actions/workflows/test.yml)
|
|
7
7
|
[](https://github.com/sakikoTGW/dsh-plugin-resolver/actions/workflows/ablation.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/dsh-plugin-resolver)
|
|
8
9
|
[](LICENSE)
|
|
9
10
|
[](package.json)
|
|
10
11
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -20,13 +21,13 @@
|
|
|
20
21
|
|
|
21
22
|
## 为什么要它
|
|
22
23
|
|
|
23
|
-
**场景**:给一台 DSH
|
|
24
|
+
**场景**:给一台 DSH 装两个真实插件——`dsh-better-sidebar`(npm 上的 latest 不保证兼容当前内核)和 `dsh-browser`(6 个同名 owner、GitHub-only、还要本地构建扩展)。当时在 agent 侧,这是这样一串手工活:
|
|
24
25
|
|
|
25
26
|
| 实际会发生的事 | 代价(真机实测) |
|
|
26
27
|
|---|---|
|
|
27
28
|
| 目录在 GitHub Pages 上,拉一次 6.4 MB | **91 s**,模型还得读一堆条目 |
|
|
28
29
|
| 同一个名字有多个 owner(`dsh-browser` 有 6 个) | 靠猜,猜错就装错来源 |
|
|
29
|
-
| `latest`
|
|
30
|
+
| `latest` 未必兼容当前内核(peer 线或 `engines.dsh` 可能高于宿主) | 装上后启动预检把它静默禁用 |
|
|
30
31
|
| 直连 npmjs 慢 | 两次 **90 s 超时**,换镜像后 13.4 s |
|
|
31
32
|
| GitHub-only 仓库不提交构建产物 | `dsh plugin add github:…` 装了个 `main` 指向不存在文件的包 |
|
|
32
33
|
| 装完不知道有没有生效 | 需要另找手段回读 loader entry / 前端 boot 表 |
|
|
@@ -55,12 +56,12 @@ flowchart LR
|
|
|
55
56
|
要求 **DSH `>=0.2.0-rc.2 <0.3.0`**(已在 `engines.dsh` 声明)。纯 JS、无构建、无 install 脚本,三种方式都不需要 `allowBuilds` 授权:
|
|
56
57
|
|
|
57
58
|
```sh
|
|
58
|
-
# npm
|
|
59
|
-
dsh plugin --profile
|
|
59
|
+
# npm(已发布 0.1.2;profile 名以你的 DSH 安装为准)
|
|
60
|
+
dsh plugin --profile <profile> add dsh-plugin-resolver
|
|
60
61
|
# GitHub 源码安装(建议锁 commit)
|
|
61
|
-
dsh plugin --profile
|
|
62
|
+
dsh plugin --profile <profile> add github:sakikoTGW/dsh-plugin-resolver#<commit>
|
|
62
63
|
# tarball
|
|
63
|
-
npm pack && dsh plugin --profile
|
|
64
|
+
npm pack && dsh plugin --profile <profile> add ./dsh-plugin-resolver-0.1.2.tgz
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
装完(宿主重启后最稳)直接说:
|
|
@@ -68,7 +69,7 @@ npm pack && dsh plugin --profile web add ./dsh-plugin-resolver-0.1.0.tgz
|
|
|
68
69
|
> "帮我装 dsh-browser" → 它会先问/列出 6 个同名候选;
|
|
69
70
|
> "装 dsh-builtin-browser" → 自动选到与你内核兼容的版本并装好。
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
插件市场(Discover)索引(在插件页打开实验性「自定义插件市场」,然后 `插件 → 发现 → 添加索引源`):
|
|
72
73
|
|
|
73
74
|
```text
|
|
74
75
|
https://sakikoTGW.github.io/dsh-plugin-resolver/market/index.json
|
|
@@ -78,19 +79,18 @@ https://sakikoTGW.github.io/dsh-plugin-resolver/market/index.json
|
|
|
78
79
|
|
|
79
80
|
```text
|
|
80
81
|
$ plugin_resolve dsh-builtin-browser
|
|
81
|
-
target kind: npm running DSH: 0.2.
|
|
82
|
+
target kind: npm running DSH: 0.2.1-rc.2
|
|
82
83
|
package: dsh-builtin-browser@0.4.13
|
|
83
84
|
install spec: dsh-builtin-browser@0.4.13
|
|
84
85
|
registry: https://registry.npmmirror.com/
|
|
85
86
|
notes:
|
|
86
|
-
- newest
|
|
87
|
+
- newest published version whose DSH peer range accepts the running host
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
```text
|
|
90
91
|
$ plugin_resolve dsh-better-sidebar
|
|
91
|
-
package: dsh-better-sidebar@0.
|
|
92
|
-
|
|
93
|
-
- 0.28.0: @deepseek-ai/dsh-agent needs ^0.2.1-rc.1, host is 0.2.0-rc.2
|
|
92
|
+
package: dsh-better-sidebar@0.28.0
|
|
93
|
+
install spec: dsh-better-sidebar@0.28.0
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
## 四个工具
|
|
@@ -115,6 +115,20 @@ skipped versions:
|
|
|
115
115
|
- **GitHub-only**:检出到 `$DSH_HOME/plugin-sources/<owner>__<repo>`,缺构建产物且声明了 `build` 时执行 `pnpm install` + `pnpm run build`,随后 `installBundle('link:<pkgDir>')`,已构建的检出直接复用;
|
|
116
116
|
- **显式 spec**(`github:` / `link:` / `file:` / `https:`):原样交给插件管理器。
|
|
117
117
|
|
|
118
|
+
## 重启判定
|
|
119
|
+
|
|
120
|
+
`plugin_install` 不再无条件要求重启;安装前后会比较:
|
|
121
|
+
|
|
122
|
+
- `pluginManager.listBundles()` 的 bundle 集合
|
|
123
|
+
- 已安装包运行时文件的 hash(排除 README / docs / 版本号)
|
|
124
|
+
|
|
125
|
+
结果:
|
|
126
|
+
|
|
127
|
+
- 无变化 / 仅版本号变化 / 管理器已就地应用 → `restart: not required`
|
|
128
|
+
- bundle 新增、运行时代码变化、client 半变化 → `restart: required — <原因>`
|
|
129
|
+
|
|
130
|
+
本插件自身的 `logic/` 改动可以按 mtime 热加载,不需要重启;新增 bundle 或宿主壳变化仍然必须重启。
|
|
131
|
+
|
|
118
132
|
## 结构:稳定壳 + 可热加载逻辑
|
|
119
133
|
|
|
120
134
|
```
|
|
@@ -128,7 +142,7 @@ logic/ 目录 / 版本 / 兼容 / 安装编排(每次调用按 mti
|
|
|
128
142
|
- **改 `logic/` 任何文件 → 下一次工具调用立即生效,不用重启宿主**
|
|
129
143
|
- 只有改动工具定义层(`index.js` / `host/register.js`)才需要重启
|
|
130
144
|
|
|
131
|
-
|
|
145
|
+
这是为无 HMR 的打包运行环境设计的:宿主拿不到真 HMR 所需的 Node 内部模块加载器,但"重新 import 一个带新 query 的模块"完全可行。
|
|
132
146
|
|
|
133
147
|
## 效果
|
|
134
148
|
|
|
@@ -150,7 +164,7 @@ node test/ablation.mjs # 消融测试(无副作用)
|
|
|
150
164
|
## 测试与 CI
|
|
151
165
|
|
|
152
166
|
```sh
|
|
153
|
-
npm test # = selftest(
|
|
167
|
+
npm test # = selftest(25) + harness(10)
|
|
154
168
|
npm run bench # 基准
|
|
155
169
|
npm run ablate # 消融
|
|
156
170
|
```
|
|
@@ -163,10 +177,10 @@ npm run ablate # 消融
|
|
|
163
177
|
## 已知限制
|
|
164
178
|
|
|
165
179
|
- 兼容性以 npm 元数据里的 peer / `engines.dsh` 为准,宿主启动预检另有判断时以宿主为准;
|
|
166
|
-
- **替换已加载插件**仍需要重启宿主(loader 模块缓存 +
|
|
180
|
+
- **替换已加载插件**仍需要重启宿主(loader 模块缓存 + 打包宿主无 Node 内部 loader);`plugin_install` 会先比较安装前后的 bundle 集合与运行时文件 hash,只有真正变化时才报 `restart-required`;
|
|
167
181
|
- 同名歧义不猜,需要调用方给 `owner=` 或完整 spec;
|
|
168
182
|
- 目录是社区维护的第三方列表,插件只保证"按声明来源装对版本",不做安全审计;
|
|
169
|
-
- GitHub
|
|
183
|
+
- GitHub 源码构建依赖宿主环境里的 `pnpm`(PATH 或 `DSH_PNPM_PATH`)与 `tar`,大仓库首次构建较慢。
|
|
170
184
|
|
|
171
185
|
## License
|
|
172
186
|
|
package/logic/api.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
const __v = new URL(import.meta.url).searchParams.get('v') ?? ''
|
|
16
16
|
const __q = (name) => `./${name}${__v === '' ? '' : `?v=${encodeURIComponent(__v)}`}`
|
|
17
17
|
const { baseName, loadCatalog, searchCatalog } = await import(__q('catalog.js'))
|
|
18
|
-
const { installGithubSource, installRegistry, verifyInstall } = await import(__q('install.js'))
|
|
18
|
+
const { installGithubSource, installRegistry, verifyInstall, capturePackageRuntime, decideRestart } = await import(__q('install.js'))
|
|
19
19
|
const { resolveTarget } = await import(__q('resolve.js'))
|
|
20
20
|
const { oneLine, stars } = await import(__q('util.js'))
|
|
21
21
|
|
|
@@ -122,6 +122,16 @@ export async function pluginInstall(ctx, manager, options, args) {
|
|
|
122
122
|
let packageName = plan.name
|
|
123
123
|
let steps = []
|
|
124
124
|
let application = 'unknown'
|
|
125
|
+
let beforeRuntime = null
|
|
126
|
+
let bundlesBefore = []
|
|
127
|
+
if (plan.kind === 'npm' && typeof plan.name === 'string' && plan.name !== '') {
|
|
128
|
+
beforeRuntime = await capturePackageRuntime(plan.name).catch(() => null)
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
bundlesBefore = (await manager.listBundles()).map((bundle) => bundle.name)
|
|
132
|
+
} catch {
|
|
133
|
+
bundlesBefore = []
|
|
134
|
+
}
|
|
125
135
|
|
|
126
136
|
if (plan.kind === 'github') {
|
|
127
137
|
const result = await installGithubSource(manager, plan, {
|
|
@@ -140,10 +150,11 @@ export async function pluginInstall(ctx, manager, options, args) {
|
|
|
140
150
|
if (steps.length > 0) lines.push(`steps: ${steps.join(' → ')}`)
|
|
141
151
|
} else {
|
|
142
152
|
const { summary } = await installRegistry(manager, plan)
|
|
143
|
-
|
|
153
|
+
const alreadyInstalled = /already-installed/i.test(summary.error ?? '')
|
|
154
|
+
application = alreadyInstalled ? 'applied' : summary.application
|
|
144
155
|
if (packageName === null && summary.bundle) packageName = summary.bundle
|
|
145
|
-
lines.push(`install: ${summary.application} (${summary.stage})`)
|
|
146
|
-
if (summary.error) lines.push(`error: ${summary.error}`)
|
|
156
|
+
lines.push(alreadyInstalled ? 'install: already installed (no change)' : `install: ${summary.application} (${summary.stage})`)
|
|
157
|
+
if (summary.error && !alreadyInstalled) lines.push(`error: ${summary.error}`)
|
|
147
158
|
if (summary.pendingBuilds.length > 0) {
|
|
148
159
|
lines.push(`pending build scripts (needs approval): ${summary.pendingBuilds.join(', ')}`)
|
|
149
160
|
}
|
|
@@ -163,14 +174,26 @@ export async function pluginInstall(ctx, manager, options, args) {
|
|
|
163
174
|
lines.push(`- bundle: ${health.bundle.name}@${health.bundle.version ?? '?'} enabled=${health.bundle.enabled}`)
|
|
164
175
|
}
|
|
165
176
|
lines.push(`- package: ${packageName}${health.hasClient ? ` (client half via ${health.clientSource})` : ' (host-only)'}`)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
let afterRuntime = null
|
|
178
|
+
let bundlesAfter = []
|
|
179
|
+
try {
|
|
180
|
+
afterRuntime = await capturePackageRuntime(packageName)
|
|
181
|
+
bundlesAfter = (await manager.listBundles()).map((bundle) => bundle.name)
|
|
182
|
+
} catch {
|
|
183
|
+
// keep the manager's own restart decision
|
|
169
184
|
}
|
|
185
|
+
const restart = decideRestart({ before: beforeRuntime, after: afterRuntime, bundlesBefore, bundlesAfter, managerApplication: application })
|
|
186
|
+
lines.push('', `restart: ${restart.required ? 'required' : 'not required'} — ${restart.reason}`)
|
|
187
|
+
if (application === 'restart-required' && !restart.required) {
|
|
188
|
+
lines.push('- note: manager requested a restart, but runtime files are unchanged')
|
|
189
|
+
}
|
|
190
|
+
if (!restart.required && restart.versionChanged) lines.push('- version label updates after the next restart')
|
|
191
|
+
const next = []
|
|
192
|
+
if (restart.required) next.push(`restart DSH fully: ${restart.reason}`)
|
|
170
193
|
if (health.entry?.fiberPhase === 'failed') next.push('host half failed to load; inspect the harness log for the error')
|
|
171
|
-
if (
|
|
172
|
-
if (health.entry?.fiberPhase === 'active' && !health.hasClient &&
|
|
173
|
-
next.push('host-only: no GUI refresh required')
|
|
194
|
+
if (restart.clientChanged) next.push('client half changed: refresh the GUI (Ctrl+R) to load it')
|
|
195
|
+
if (health.entry?.fiberPhase === 'active' && !health.hasClient && !restart.required && !restart.codeChanged) {
|
|
196
|
+
next.push('host-only: no restart or GUI refresh required')
|
|
174
197
|
}
|
|
175
198
|
if (next.length > 0) lines.push('', `next: ${next.join('; ')}`)
|
|
176
199
|
}
|
package/logic/install.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
const __v = new URL(import.meta.url).searchParams.get('v') ?? ''
|
|
3
|
-
const __q = (name) => `./${name}${__v === '' ? '' : `?v=${encodeURIComponent(__v)}`}`
|
|
4
|
-
const { dshHome, dirExists, runProcess } = await import(__q('util.js'))
|
|
5
|
-
const { registryBase } = await import(__q('resolve.js'))
|
|
1
|
+
// Reloadable module: local dependencies carry this module's `?v=` query.
|
|
2
|
+
const __v = new URL(import.meta.url).searchParams.get('v') ?? ''
|
|
3
|
+
const __q = (name) => `./${name}${__v === '' ? '' : `?v=${encodeURIComponent(__v)}`}`
|
|
4
|
+
const { dshHome, dirExists, runProcess } = await import(__q('util.js'))
|
|
5
|
+
const { registryBase } = await import(__q('resolve.js'))
|
|
6
6
|
/**
|
|
7
7
|
* Installation: drive the profile's plugin manager, build GitHub-only sources,
|
|
8
8
|
* and verify what actually landed.
|
|
@@ -17,6 +17,7 @@ const { registryBase } = await import(__q('resolve.js'))
|
|
|
17
17
|
|
|
18
18
|
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
19
19
|
import { existsSync } from 'node:fs'
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
20
21
|
import path from 'node:path'
|
|
21
22
|
|
|
22
23
|
/** Longest a source install may run before the tool reports a timeout. */
|
|
@@ -95,21 +96,138 @@ export async function installRegistry(manager, plan) {
|
|
|
95
96
|
* @returns {Promise<any | undefined>}
|
|
96
97
|
*/
|
|
97
98
|
export async function installedManifest(packageName) {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
99
|
+
const packageDir = await installedPackageDir(packageName)
|
|
100
|
+
if (packageDir === undefined) return undefined
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'))
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Candidate profile directories, most specific first. */
|
|
109
|
+
function profileDirs() {
|
|
110
|
+
const dirs = []
|
|
111
|
+
if (process.env.DSH_PROFILE_DIR) dirs.push(process.env.DSH_PROFILE_DIR)
|
|
112
|
+
const home = process.env.DSH_HOME && process.env.DSH_HOME !== '' ? process.env.DSH_HOME : dshHome()
|
|
101
113
|
if (home) {
|
|
102
|
-
const profile = process.env.DSH_PROFILE && process.env.DSH_PROFILE !== '' ? process.env.DSH_PROFILE : '
|
|
103
|
-
|
|
114
|
+
const profile = process.env.DSH_PROFILE && process.env.DSH_PROFILE !== '' ? process.env.DSH_PROFILE : 'desktop'
|
|
115
|
+
for (const name of [profile, 'desktop', 'web']) dirs.push(path.join(home, 'profiles', name))
|
|
116
|
+
}
|
|
117
|
+
return [...new Set(dirs)]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Locate an installed package directory without assuming one profile layout.
|
|
122
|
+
* @param {string} packageName
|
|
123
|
+
* @returns {Promise<string | undefined>}
|
|
124
|
+
*/
|
|
125
|
+
export async function installedPackageDir(packageName) {
|
|
126
|
+
for (const profileDir of profileDirs()) {
|
|
127
|
+
const candidate = path.join(profileDir, 'node_modules', packageName)
|
|
128
|
+
if (existsSync(path.join(candidate, 'package.json'))) return candidate
|
|
104
129
|
}
|
|
105
|
-
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sha256(value) {
|
|
134
|
+
return createHash('sha256').update(value).digest('hex')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Runtime files that decide whether the loaded code actually changed. */
|
|
138
|
+
async function hashRuntimeFiles(dir) {
|
|
139
|
+
const files = []
|
|
140
|
+
async function walk(current) {
|
|
141
|
+
let entries
|
|
106
142
|
try {
|
|
107
|
-
|
|
143
|
+
entries = await readdir(current, { withFileTypes: true })
|
|
108
144
|
} catch {
|
|
109
|
-
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (entry.name === 'node_modules' || entry.name === 'test' || entry.name === 'tests' || entry.name === 'fixtures' || entry.name === 'examples' || entry.name === '.git' || entry.name === '.github') continue
|
|
149
|
+
if (/^(license|notice|readme)/i.test(entry.name)) continue
|
|
150
|
+
const abs = path.join(current, entry.name)
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
await walk(abs)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (!entry.isFile()) continue
|
|
156
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
157
|
+
if (ext === '.md' || ext === '.markdown' || ext === '.map' || ext === '.d.ts' || ext === '.pdb') continue
|
|
158
|
+
if (entry.name === 'package.json') continue
|
|
159
|
+
try {
|
|
160
|
+
files.push(`${path.relative(dir, abs).split(path.sep).join('/')}:${sha256(await readFile(abs))}`)
|
|
161
|
+
} catch {
|
|
162
|
+
// unreadable runtime file: treating it as absent is safer than crashing
|
|
163
|
+
}
|
|
110
164
|
}
|
|
111
165
|
}
|
|
112
|
-
|
|
166
|
+
await walk(dir)
|
|
167
|
+
files.sort()
|
|
168
|
+
return files
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Snapshot the installed package's runtime identity. README/docs and the
|
|
173
|
+
* version field are excluded: those changes do not alter loaded code.
|
|
174
|
+
* @param {string} packageName
|
|
175
|
+
* @returns {Promise<{version: string, hash: string, clientHash: string, hasClient: boolean, dir: string} | null>}
|
|
176
|
+
*/
|
|
177
|
+
export async function capturePackageRuntime(packageName) {
|
|
178
|
+
const dir = await installedPackageDir(packageName)
|
|
179
|
+
if (dir === undefined) return null
|
|
180
|
+
try {
|
|
181
|
+
const manifest = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8'))
|
|
182
|
+
const files = await hashRuntimeFiles(dir)
|
|
183
|
+
const meta = {
|
|
184
|
+
main: manifest.main ?? null,
|
|
185
|
+
exports: manifest.exports ?? null,
|
|
186
|
+
dsh: manifest.dsh ?? null,
|
|
187
|
+
dependencies: manifest.dependencies ?? {},
|
|
188
|
+
peerDependencies: manifest.peerDependencies ?? {},
|
|
189
|
+
}
|
|
190
|
+
const clientFiles = files.filter((line) => /(^|[/\\])[^/\\]*client[^/\\]*\.(?:js|mjs|cjs|ts):/i.test(line))
|
|
191
|
+
return {
|
|
192
|
+
version: String(manifest.version ?? ''),
|
|
193
|
+
hash: sha256(`${JSON.stringify(meta)}\n${files.join('\n')}`),
|
|
194
|
+
clientHash: sha256(clientFiles.join('\n')),
|
|
195
|
+
hasClient: Boolean(manifest.dsh?.client),
|
|
196
|
+
dir,
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Decide whether install results really require a restart. Bundle additions and
|
|
205
|
+
* changed runtime/client files do; no-op reinstall and metadata-only version
|
|
206
|
+
* bumps do not. A manager-applied in-place change needs no restart either.
|
|
207
|
+
* @param {{before: any, after: any, bundlesBefore: string[], bundlesAfter: string[], managerApplication: string}} input
|
|
208
|
+
*/
|
|
209
|
+
export function decideRestart({ before, after, bundlesBefore, bundlesAfter, managerApplication }) {
|
|
210
|
+
const beforeNames = new Set(bundlesBefore ?? [])
|
|
211
|
+
const afterNames = new Set(bundlesAfter ?? [])
|
|
212
|
+
const added = [...afterNames].filter((name) => !beforeNames.has(name))
|
|
213
|
+
const removed = [...beforeNames].filter((name) => !afterNames.has(name))
|
|
214
|
+
if (added.length > 0 || removed.length > 0) {
|
|
215
|
+
return { required: true, reason: `bundle set changed (${[...added, ...removed].join(', ')})`, bundleChanged: true, codeChanged: true, clientChanged: false, versionChanged: false }
|
|
216
|
+
}
|
|
217
|
+
if (!before || !after) {
|
|
218
|
+
const required = managerApplication === 'restart-required'
|
|
219
|
+
return { required, reason: required ? 'manager requested a restart (no runtime snapshot)' : 'manager applied it in place', bundleChanged: false, codeChanged: false, clientChanged: false, versionChanged: false }
|
|
220
|
+
}
|
|
221
|
+
const versionChanged = before.version !== after.version
|
|
222
|
+
const codeChanged = before.hash !== after.hash
|
|
223
|
+
const clientChanged = Boolean(before.hasClient && after.hasClient && before.clientHash !== after.clientHash)
|
|
224
|
+
if (!codeChanged) {
|
|
225
|
+
return { required: false, reason: versionChanged ? 'runtime files are unchanged (version label only)' : 'runtime files are unchanged', bundleChanged: false, codeChanged: false, clientChanged: false, versionChanged }
|
|
226
|
+
}
|
|
227
|
+
if (managerApplication === 'applied') {
|
|
228
|
+
return { required: false, reason: 'manager applied the change in place', bundleChanged: false, codeChanged: true, clientChanged, versionChanged }
|
|
229
|
+
}
|
|
230
|
+
return { required: true, reason: clientChanged ? 'client half changed' : 'runtime code changed', bundleChanged: false, codeChanged: true, clientChanged, versionChanged }
|
|
113
231
|
}
|
|
114
232
|
|
|
115
233
|
/**
|
|
@@ -162,14 +280,8 @@ export async function verifyInstall(ctx, manager, packageName) {
|
|
|
162
280
|
|
|
163
281
|
/** Locate a pnpm executable usable for a source build. */
|
|
164
282
|
export function resolvePnpm() {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
process.platform === 'win32'
|
|
168
|
-
? [path.join(home, '.desktop-bin', 'pnpm.cmd'), path.join(home, '..', '..', '.desktop-bin', 'pnpm.cmd')]
|
|
169
|
-
: [path.join(home, '.desktop-bin', 'pnpm')]
|
|
170
|
-
for (const candidate of candidates) {
|
|
171
|
-
if (existsSync(candidate)) return candidate
|
|
172
|
-
}
|
|
283
|
+
const explicit = process.env.DSH_PNPM_PATH
|
|
284
|
+
if (typeof explicit === 'string' && explicit !== '' && existsSync(explicit)) return explicit
|
|
173
285
|
return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
|
174
286
|
}
|
|
175
287
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-resolver",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Agent-facing DSH plugin resolver: search the catalog, pick a version the running DSH accepts, install (registry or GitHub-source build), and verify - no trial and error.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|