dsh-pwsh-plugin 0.0.0-stage → 0.2.13
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/FINDINGS.md +1789 -0
- package/LICENSE +21 -0
- package/README.md +431 -2
- package/README.zh-CN.md +386 -0
- package/cordis.patch.yml +27 -0
- package/docs/llrun.md +199 -0
- package/icon.svg +5 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +7 -0
- package/package.json +57 -4
- package/plugin/auto-update.mjs +210 -0
- package/plugin/index.mjs +580 -0
- package/plugin/session-server.ps1 +225 -0
- package/plugin/session.mjs +223 -0
- package/plugin/update-apply.mjs +274 -0
- package/plugin/update-check.mjs +270 -0
- package/wsl/acceptance.sh +155 -0
- package/wsl/compaction-epoch.mjs +115 -0
- package/wsl/custom-bash.mjs +355 -0
- package/wsl/llrun +0 -0
- package/wsl/llrun.c +337 -0
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-pwsh-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.2.13",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Persistent pwsh session tool (pwsh_run) for DeepSeek Harness: run PowerShell scripts with zero escaping, no 32K command-line cap, and state that survives across calls.",
|
|
6
|
+
"icon": "./icon.svg",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"deepseek-harness",
|
|
11
|
+
"powershell",
|
|
12
|
+
"pwsh",
|
|
13
|
+
"persistent-session"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/citydirector/dsh-pwsh-plugin.git"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./plugin/index.mjs",
|
|
22
|
+
"./session": "./plugin/session.mjs",
|
|
23
|
+
"./update": "./plugin/update-check.mjs",
|
|
24
|
+
"./wsl/custom-bash.mjs": "./wsl/custom-bash.mjs",
|
|
25
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
26
|
+
"./locale/*.json": "./locale/*.json",
|
|
27
|
+
"./icon": "./icon.svg",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"plugin",
|
|
32
|
+
"wsl",
|
|
33
|
+
"docs",
|
|
34
|
+
"locale",
|
|
35
|
+
"icon.svg",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"README.md",
|
|
38
|
+
"README.zh-CN.md",
|
|
39
|
+
"FINDINGS.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"main": "./plugin/index.mjs",
|
|
43
|
+
"dsh": {
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": "./cordis.patch.yml"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node test/itest.mjs && node test/plugintest.mjs && node test/updatetest.mjs && node test/autoupdatetest.mjs && node test/wiringtest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
|
|
56
|
+
"test:all": "node test/itest.mjs && node test/plugintest.mjs && node test/e2e-real-subprocess.mjs && node test/sandboxtest.mjs && node test/bashtest.mjs && node test/bash-e2e.mjs && node test/updatetest.mjs && node test/autoupdatetest.mjs && node test/wiringtest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
|
|
57
|
+
"test:update": "node test/updatetest.mjs && node test/autoupdatetest.mjs && node test/wiringtest.mjs"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 无感自动更新:由插件自己把新版本装上,并让它就地生效。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不能直接调 installBundle
|
|
5
|
+
* 插件的 apply 与它起的定时器都跑在 HMR 的事务上下文里(AsyncLocalStorage)。
|
|
6
|
+
* 在那个上下文里调 installBundle,pnpm 会跑完、磁盘也会换成目标版本,但随后的
|
|
7
|
+
* 热重组会被 runExclusive 的嵌套检查拒掉(HMR transactions cannot be nested),
|
|
8
|
+
* 留下「磁盘新版、进程旧版」这种最难看清的状态(实测)。先 executing.run(undefined, …)
|
|
9
|
+
* 把上下文清空再调,HMR 才会真的重组合,并顺手把本插件自己替换掉
|
|
10
|
+
* (实测:改完文件后 137ms 新代码就生效,无需重启)。
|
|
11
|
+
*
|
|
12
|
+
* 为什么没有 HMR 就完全不装
|
|
13
|
+
* 没有 HMR 的宿主上,装完磁盘是新的、进程里仍是旧的,而 bundle 行给不出新的 module URL,
|
|
14
|
+
* 重启之前一直是「装上了但没生效」。这种自动毫无意义,反而让 pwsh_run_update 报出一个
|
|
15
|
+
* 与真正在跑的代码不符的版本。此时退回只提示,由人决定何时升级、何时重启。
|
|
16
|
+
*
|
|
17
|
+
* 为什么不自己下载覆盖文件
|
|
18
|
+
* 见 update-apply.mjs 的头注释:本插件是 profile 的 bundle 依赖,版本钉在
|
|
19
|
+
* package.json 与 pnpm-lock.yaml 里,绕开包管理器改文件会立刻让 lockfile 与磁盘不一致。
|
|
20
|
+
*
|
|
21
|
+
* 为什么必须等到「安静」才装
|
|
22
|
+
* 热替换会执行本插件的 effect 清理函数,那里要 dispose 掉所有常驻 pwsh 会话。
|
|
23
|
+
* 若此刻正有一条调用在跑,它会连带被掐断——脚本跑到一半、半截输出、会话状态全丢,
|
|
24
|
+
* 与直接断电没有区别。所以安装前要问宿主「现在安静吗」,忙就整个推迟,
|
|
25
|
+
* 而不是装完再说「可惜刚才那条被打断了」。
|
|
26
|
+
*
|
|
27
|
+
* 为什么要记住「上一次实际装到了哪个版本」
|
|
28
|
+
* pnpm 的 minimumReleaseAge 会让「请求 0.2.13、装到 0.2.12」成为正常结果。若不留这个记忆,
|
|
29
|
+
* 下一轮还会请求 0.2.13、还会落回 0.2.12,被 plugin-manager 判 already-installed,
|
|
30
|
+
* 退避爬到上限,永远如此。记住之后就能认出「这是冷却期,不是故障」,直接等到期。
|
|
31
|
+
*
|
|
32
|
+
* 为什么冷却期落地之后要把目标版本从「装过」里撤出来
|
|
33
|
+
* 落到更旧的版本算成功,但它不是这次的目标:目标自己还要等窗口过去。若不当场撤掉,
|
|
34
|
+
* 这个版本就再也不会被尝试,等于把这个进程的生命周期钉死在旧版本上。
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { installRelease, preflightInstall } from './update-apply.mjs'
|
|
38
|
+
import { compareVersions } from './update-check.mjs'
|
|
39
|
+
|
|
40
|
+
/** 首次检查前的等待:让启动流程先跑完,别跟它抢 pnpm。 */
|
|
41
|
+
const FIRST_DELAY_MS = 90 * 1000
|
|
42
|
+
/** 一轮正常节奏:与 UpdateChecker 的定时一致,失败时按下面的退避重排。 */
|
|
43
|
+
const INTERVAL_MS = 12 * 60 * 60 * 1000
|
|
44
|
+
const RETRY_BASE_MS = 5 * 60 * 1000
|
|
45
|
+
const RETRY_MAX_MS = 2 * 60 * 60 * 1000
|
|
46
|
+
/** 查出有新版但有调用正跑着:过一会儿再看一眼,别等到下一轮 12 小时。 */
|
|
47
|
+
const BUSY_RETRY_MS = 3 * 60 * 1000
|
|
48
|
+
/** 冷却期:pnpm 的窗口默认 24 小时,隔一小时再看一眼不浪费什么。 */
|
|
49
|
+
const COOLDOWN_RETRY_MS = 60 * 60 * 1000
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 在 HMR 事务之外装一个发布。
|
|
53
|
+
*
|
|
54
|
+
* @param ctx - 插件上下文,用来取 pluginManager 与 hmr。
|
|
55
|
+
* @param spec - 钉住版本号的安装规格,例如 dsh-pwsh-plugin@0.2.13。
|
|
56
|
+
* @param options - { requestId, log }。
|
|
57
|
+
* @returns 与 installRelease 相同的结果对象,永不抛出。
|
|
58
|
+
*/
|
|
59
|
+
export async function installReleaseLive(ctx, spec, options = {}) {
|
|
60
|
+
const manager = ctx?.get?.('pluginManager')
|
|
61
|
+
const hmr = ctx?.get?.('hmr')
|
|
62
|
+
// 回读装完的版本需要 profile 目录;读不到就只报 application,不编一个版本号出来。
|
|
63
|
+
const profileDir = ctx?.get?.('profileContext')?.dir
|
|
64
|
+
const base = { ...options, ...(typeof profileDir === 'string' ? { profileDir } : {}) }
|
|
65
|
+
if (hmr === undefined || typeof hmr?.executing?.run !== 'function') {
|
|
66
|
+
return await installRelease(manager, spec, { ...base, live: false })
|
|
67
|
+
}
|
|
68
|
+
// 上下文清空后 installBundle 自己会拿 runExclusive;装完 HMR 重组合,
|
|
69
|
+
// 本插件的模块被驱逐、新模块 import 进来重新 apply。
|
|
70
|
+
return await hmr.executing.run(undefined, () => installRelease(manager, spec, { ...base, live: true }))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 宿主能不能就地换掉已加载的模块。判断与 installReleaseLive 用同一组条件,
|
|
75
|
+
* 两处必须一致,否则会出现「循环以为装得上、安装却退回非 live」的错配。
|
|
76
|
+
*
|
|
77
|
+
* @param ctx - 插件上下文。
|
|
78
|
+
* @returns 有可用的 HMR 服务时为 true。
|
|
79
|
+
*/
|
|
80
|
+
function hmrIsLive(ctx) {
|
|
81
|
+
const hmr = ctx?.get?.('hmr')
|
|
82
|
+
return hmr !== undefined && typeof hmr?.executing?.run === 'function'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 起一个自动更新循环:查到新版本就装上,失败按指数退避重试。
|
|
87
|
+
*
|
|
88
|
+
* @param ctx - 插件上下文。
|
|
89
|
+
* @param checker - UpdateChecker 实例;它的 check() 重入安全,直接复用。
|
|
90
|
+
* @param options - { log, firstDelayMs, intervalMs, retryBaseMs, retryMaxMs, cooldownRetryMs }。
|
|
91
|
+
* @returns 停止函数;插件被卸载或热替换时由调用方停掉。
|
|
92
|
+
*/
|
|
93
|
+
export function startAutoUpdate(ctx, checker, options = {}) {
|
|
94
|
+
const log = options.log ?? (() => undefined)
|
|
95
|
+
const firstDelayMs = options.firstDelayMs ?? FIRST_DELAY_MS
|
|
96
|
+
const intervalMs = options.intervalMs ?? INTERVAL_MS
|
|
97
|
+
const retryBaseMs = options.retryBaseMs ?? RETRY_BASE_MS
|
|
98
|
+
const retryMaxMs = options.retryMaxMs ?? RETRY_MAX_MS
|
|
99
|
+
const cooldownRetryMs = options.cooldownRetryMs ?? COOLDOWN_RETRY_MS
|
|
100
|
+
|
|
101
|
+
const installGate = options.installGate
|
|
102
|
+
const busyRetryMs = options.busyRetryMs ?? BUSY_RETRY_MS
|
|
103
|
+
|
|
104
|
+
/** 已经自动装过的版本:同一个版本装成功过就不再装第二次。 */
|
|
105
|
+
const attempted = new Set()
|
|
106
|
+
/** 上一次安装实际落到的版本:冷却期会让它与请求的版本不同,用来认出「不是故障」。 */
|
|
107
|
+
let lastInstalled
|
|
108
|
+
let timer
|
|
109
|
+
let stopped = false
|
|
110
|
+
let running = false
|
|
111
|
+
let failures = 0
|
|
112
|
+
/** 「忙,先不装」只记一次日志:每几分钟重复一行会把日志刷满。 */
|
|
113
|
+
let busyNoted = false
|
|
114
|
+
|
|
115
|
+
const schedule = (delayMs) => {
|
|
116
|
+
if (stopped) return
|
|
117
|
+
timer = setTimeout(tick, delayMs)
|
|
118
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tick = async () => {
|
|
122
|
+
if (stopped || running) return
|
|
123
|
+
running = true
|
|
124
|
+
let next = intervalMs
|
|
125
|
+
try {
|
|
126
|
+
const state = await checker.check()
|
|
127
|
+
if (state.state === 'available' && hmrIsLive(ctx) !== true) {
|
|
128
|
+
// 装上也换不掉正在跑的模块:留着提示那条路,别制造「装了但没生效」。
|
|
129
|
+
log('auto update: ' + state.latest + ' is available, but this host runs no HMR — leaving it for a manual upgrade')
|
|
130
|
+
failures = 0
|
|
131
|
+
} else if (state.state === 'available' && !attempted.has(state.latest) && installGate?.tryAcquire() !== false) {
|
|
132
|
+
try {
|
|
133
|
+
const doc = checker.document()
|
|
134
|
+
const spec = doc.spec
|
|
135
|
+
if (typeof spec === 'string' && spec.length > 0) {
|
|
136
|
+
// 装之前先问一句值不值得装,否则 already-installed 会以「失败」的姿态出现,
|
|
137
|
+
// 而它其实有几种含义完全不同的成因(别人装好了 / 发布忘了升版本 / 冷却期未过)。
|
|
138
|
+
const unnecessary = preflightInstall(ctx, checker, spec, lastInstalled === undefined ? {} : { cooled: lastInstalled })
|
|
139
|
+
if (unnecessary !== null) {
|
|
140
|
+
busyNoted = false
|
|
141
|
+
// 记下这个版本,别再排下一轮:这几种成因都不会在几分钟内自己好转。
|
|
142
|
+
// 冷却期例外——它是会到期的,所以单独按小时再看。
|
|
143
|
+
attempted.add(state.latest)
|
|
144
|
+
log('auto update: ' + state.latest + ' — ' + unnecessary.outcome + ': ' + unnecessary.text)
|
|
145
|
+
if (unnecessary.outcome === 'cooling-down') {
|
|
146
|
+
attempted.delete(state.latest)
|
|
147
|
+
next = cooldownRetryMs
|
|
148
|
+
} else if (unnecessary.ok === true) {
|
|
149
|
+
failures = 0
|
|
150
|
+
}
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
busyNoted = false
|
|
154
|
+
attempted.add(state.latest)
|
|
155
|
+
log('auto update: installing ' + state.latest + ' from ' + spec)
|
|
156
|
+
const outcome = await installReleaseLive(ctx, spec, { requestId: 'pwsh-run-auto-update', log })
|
|
157
|
+
log('auto update: ' + outcome.outcome + ' — ' + outcome.text)
|
|
158
|
+
if (outcome.installedVersion !== undefined) {
|
|
159
|
+
lastInstalled = outcome.installedVersion
|
|
160
|
+
// 落到更旧的版本是 pnpm 冷却期的正常结果,但目标版本还在窗口里:
|
|
161
|
+
// 撤掉这条记录,过一小时(窗口是 24 小时)再看,而不是等满 12 小时。
|
|
162
|
+
if (compareVersions(outcome.installedVersion, state.latest) < 0) {
|
|
163
|
+
attempted.delete(state.latest)
|
|
164
|
+
next = cooldownRetryMs
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (outcome.ok !== true) {
|
|
168
|
+
// 装失败要能重来:把记录撤掉,交给退避后的下一轮。
|
|
169
|
+
attempted.delete(state.latest)
|
|
170
|
+
failures += 1
|
|
171
|
+
next = Math.min(retryMaxMs, retryBaseMs * 2 ** (failures - 1))
|
|
172
|
+
} else {
|
|
173
|
+
failures = 0
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
// 安装可能把本插件本身驱逐掉,于是这个 release 落在旧实例的闭包上,
|
|
178
|
+
// 只是把旧变量置回 false——新实例有它自己的闸门。
|
|
179
|
+
installGate?.release()
|
|
180
|
+
}
|
|
181
|
+
} else if (state.state === 'available' && !attempted.has(state.latest)) {
|
|
182
|
+
// 有新版,但此刻有调用在跑:热替换会掐断它。整轮推迟,几分钟后再看。
|
|
183
|
+
if (!busyNoted) {
|
|
184
|
+
busyNoted = true
|
|
185
|
+
log('auto update: ' + state.latest + ' is available, but a call is running — postponing the install until the session is idle')
|
|
186
|
+
}
|
|
187
|
+
next = busyRetryMs
|
|
188
|
+
} else if (state.state === 'failed') {
|
|
189
|
+
failures += 1
|
|
190
|
+
next = Math.min(retryMaxMs, retryBaseMs * 2 ** (failures - 1))
|
|
191
|
+
} else {
|
|
192
|
+
failures = 0
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// check() 本身不抛;这里兜的是 installReleaseLive 之外的意外。
|
|
196
|
+
failures += 1
|
|
197
|
+
next = Math.min(retryMaxMs, retryBaseMs * 2 ** (failures - 1))
|
|
198
|
+
log('auto update: unexpected failure — ' + String(error?.message ?? error))
|
|
199
|
+
} finally {
|
|
200
|
+
running = false
|
|
201
|
+
schedule(next)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
schedule(firstDelayMs)
|
|
206
|
+
return () => {
|
|
207
|
+
stopped = true
|
|
208
|
+
clearTimeout(timer)
|
|
209
|
+
}
|
|
210
|
+
}
|