@mzzsfy/dsh-shell-select 0.4.7 → 0.4.9
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 +2 -0
- package/package.json +1 -1
- package/src/apply-state.mjs +6 -0
- package/src/guard.js +26 -2
- package/src/index.js +14 -5
- package/src/prompt-projection.mjs +10 -0
- package/test/guard-entry.test.mjs +77 -1
- package/test/prompt-projection.test.mjs +29 -0
package/README.md
CHANGED
|
@@ -96,3 +96,5 @@ env 优先级(同键高右):内置覆盖集(NO_COLOR/PAGER/GIT_PAGER)< 条目 `e
|
|
|
96
96
|
## dsh 版本兼容
|
|
97
97
|
|
|
98
98
|
按仓库动态窗口验证(scripts/compat/window.mjs),不预置任何固定版本槽:主测 = 渠道 dist-tag 最新 rc,完全适配口径;基线 = npm latest,不崩溃底线口径;主测与基线同版时收敛单槽。本包声明 `dsh.hostMin = "0.1.7-rc.1"`:低于该代际的宿主 API 面未验证,compat 装载层自动排除。逐轮实测结论见 `docs/兼容性测试/逐包测试进度.md`(本地维护,不入库)与本包 `兼容性测试.md`。
|
|
99
|
+
|
|
100
|
+
dsh **0.2.2-rc.1** 实测两处代际契约变更均已适配:system-prompt assemble 的 tools 移到 waterfall 之后合并(投影面特性检测透传,过滤失效仅告警);设置写事务重评行 disabled 的 `!!js` 表达式(官方 pwsh-sandbox 被 runtime-disable 后可能被翻回启用)——guard 监视该翻回,dispose 主行执行器让位官方链,服务不真空、写事务不被拒。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzzsfy/dsh-shell-select",
|
|
3
3
|
"description": "Windows shell 接管:替换默认 pwsh 工具为可配置多客户端的 shell 工具(git bash/WSL/cmd/pwsh,自定义路径,默认客户端,模型按名选择);支持命令 deny 黑名单、会话终端卡增强和直接 pwsh 接管徽章,接管失败自动回退官方 pwsh 链",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/apply-state.mjs
CHANGED
|
@@ -32,3 +32,9 @@ export function recordTakeover(event) {
|
|
|
32
32
|
export function shellSelectTakeoverTrace() {
|
|
33
33
|
return takeoverTrace
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// 主行执行器 fiber 句柄(HOST-022 让位通道):mountExecutor 挂载成功后回填,
|
|
37
|
+
// guard 监视官方行翻回启用时 dispose 让位;执行器不在场(guard 代挂态/挂载
|
|
38
|
+
// 失败)恒 undefined,guard 零动作不与代挂官方链抢权。模块级单例,主行 fiber
|
|
39
|
+
// 生命周期内有效。
|
|
40
|
+
export const shellSelectExecutorFiber = { current: undefined }
|
package/src/guard.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { MAIN_ROW_ID, OFFICIAL_ROW_IDS, detectDeadState, rowState, rowScan } from './guard-state.mjs'
|
|
10
10
|
import { officialRowConfig, readSettingsDocument, resolveFromHostTree } from './guard-config.mjs'
|
|
11
11
|
import { officialRowState } from './takeover.mjs'
|
|
12
|
-
import { shellSelectApplyState, shellSelectTakeoverTrace } from './apply-state.mjs'
|
|
12
|
+
import { shellSelectApplyState, shellSelectTakeoverTrace, shellSelectExecutorFiber } from './apply-state.mjs'
|
|
13
13
|
|
|
14
14
|
// 官方包 npm 名(与行 id 独立:行 id 是 dsh-base 组合行,包名才是模块解析键)
|
|
15
15
|
const OFFICIAL_PACKAGES = {
|
|
@@ -171,10 +171,34 @@ export async function installGuard(ctx, {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// 复活让位:受控行任一即将 init 且本方在场,先卸代挂(waterfall 保证先于其 apply)
|
|
174
|
+
// 复活让位:受控行任一即将 init 且本方在场,先卸代挂(waterfall 保证先于其 apply)。
|
|
175
|
+
// HOST-022 分支(0.2.2 写事务重评 !!js 表达式):官方行翻回启用且主行执行器
|
|
176
|
+
// 在场(句柄在场 = 主行活态接管中),dispose 执行器让位官方链——官方行激活
|
|
177
|
+
// 与 ShellSelectExecutor 撞 ctx.shell 会炸行激活并被写入门控放大为
|
|
178
|
+
// settings/rejected;求值抛错按零动作安全向(让位制造 shell 真空,不确定时
|
|
179
|
+
// 保持现状由 sweep 兜底)。
|
|
175
180
|
const patchContextGuard = async (entry, next) => {
|
|
176
181
|
const id = entry?.options?.id
|
|
177
182
|
if (id !== MAIN_ROW_ID && !OFFICIAL_ROW_IDS.includes(id)) return next()
|
|
183
|
+
if (id !== MAIN_ROW_ID && entry.fiber?.uid == null && shellSelectExecutorFiber.current !== undefined) {
|
|
184
|
+
let state
|
|
185
|
+
try {
|
|
186
|
+
state = officialRowState(ctx.loader, id)
|
|
187
|
+
} catch {
|
|
188
|
+
state = null
|
|
189
|
+
}
|
|
190
|
+
if (state?.present === true && state.disabled === false && state.disabledUnreliable === false) {
|
|
191
|
+
const fiber = shellSelectExecutorFiber.current
|
|
192
|
+
shellSelectExecutorFiber.current = undefined
|
|
193
|
+
ctx.logger.warn(`shell-select/guard: 官方行 ${id} 翻回启用,主行执行器卸载让位(官方链接管)`)
|
|
194
|
+
try {
|
|
195
|
+
await fiber.dispose()
|
|
196
|
+
} catch (error) {
|
|
197
|
+
ctx.logger.warn(`shell-select/guard: 执行器让位 dispose 失败(${error?.message ?? error});官方行激活可能撞服务,由宿主行级失败兜底`)
|
|
198
|
+
}
|
|
199
|
+
return next()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
178
202
|
if (entry.fiber?.uid != null) return next()
|
|
179
203
|
if (mounted.size === 0 && mounting === null) return next()
|
|
180
204
|
ctx.logger.warn(`shell-select/guard: ${id} 行复活,卸代挂让位`)
|
package/src/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
takeoverDecision,
|
|
33
33
|
} from './takeover.mjs'
|
|
34
34
|
import { coerceServiceable } from './config.mjs'
|
|
35
|
+
import { shellSelectExecutorFiber } from './apply-state.mjs'
|
|
35
36
|
|
|
36
37
|
export const name = 'shell-select'
|
|
37
38
|
|
|
@@ -110,9 +111,9 @@ function unwrapConfigSnapshot(config) {
|
|
|
110
111
|
* [object Object]" 拒挂(真机实证);解包产物必须 structuredClone:ref
|
|
111
112
|
* .get() 返回 cosmokit snapshot 深冻结对象,cordis 挂载管线对 section 原地
|
|
112
113
|
* resolve,冻结 env 直接炸 "Cannot assign to read only property" 并触发
|
|
113
|
-
*
|
|
114
|
-
* volatile-
|
|
115
|
-
*
|
|
114
|
+
* 接管回滚。快照的陈旧由设置页写路径收口补偿(F18:注释曾宣称的
|
|
115
|
+
* loader/volatile-update 监听为幻影机制,apply 零宿主事件监听是锁定契约;
|
|
116
|
+
* 设置页写成功后直接经 ctx.shell.refresh 换源,见 client/写路径)。
|
|
116
117
|
*/
|
|
117
118
|
async function mountExecutor(ctx, config) {
|
|
118
119
|
// 用户声明值挂载前 coercing:任意配置(坏类型/空 shells/悬空 default/坏
|
|
@@ -128,6 +129,10 @@ async function mountExecutor(ctx, config) {
|
|
|
128
129
|
const waiting = fiber?.inject ? Object.keys(fiber.inject).join(',') : '?'
|
|
129
130
|
throw new Error(`执行器 fiber 未激活(state=${fiber?.state},inject=${waiting})`)
|
|
130
131
|
}
|
|
132
|
+
// 执行器句柄回填(HOST-022):0.2.2 写事务重评官方行 disabled 表达式可翻回
|
|
133
|
+
// 启用,官方行复活与本方执行器撞 ctx.shell——guard 监视 patch 事件,官方行
|
|
134
|
+
// 翻回启用即经此句柄 dispose 本方执行器让位。dispose 后句柄清空防重入。
|
|
135
|
+
shellSelectExecutorFiber.current = fiber
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
/**
|
|
@@ -147,8 +152,12 @@ async function runtimeTakeover(ctx, config, targets) {
|
|
|
147
152
|
}
|
|
148
153
|
const exited = await awaitOfficialExit(targets)
|
|
149
154
|
if (!exited) {
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
// 退场超时回滚已禁行(F18):本包不挂载,官方链必须复活自服务,
|
|
156
|
+
// 不留"官方已禁+本包未挂"的 shell 真空窗口
|
|
157
|
+
ctx.logger.warn('shell-select: 官方行退场超时,回滚启用并放弃接管')
|
|
158
|
+
recordTakeover({ event: 'runtime 退场超时,回滚已禁行,放弃挂载' })
|
|
159
|
+
await rollbackDisabledRows(disabled)
|
|
160
|
+
recordTakeover({ event: `rolledBack=${disabled.length}, 退场超时` })
|
|
152
161
|
return
|
|
153
162
|
}
|
|
154
163
|
try {
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
// 只收敛模型投影,不改工具注册或执行策略;旧调用保持原 PowerShell 契约。
|
|
2
|
+
// tools/sections 特性检测:0.2.2 起 system-prompt 的 assemble 把 tools 移到
|
|
3
|
+
// waterfall 之后合并,链值不再携带该字段(0.2.1 内联 orderTools)——字段缺席时
|
|
4
|
+
// 原样透传,pwsh 过滤失效只告警,绝不抛错打断回合管线(宿主升级即崩形态)。
|
|
2
5
|
export function mountShellPromptProjection(ctx) {
|
|
3
6
|
if (typeof ctx.on !== 'function') return
|
|
4
7
|
let warned = false
|
|
5
8
|
return ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
6
9
|
const result = await next()
|
|
7
10
|
if (ctx.tools.get('shell', context.scope) === undefined) return result
|
|
11
|
+
if (!Array.isArray(result.tools) || !Array.isArray(result.sections)) {
|
|
12
|
+
if (!warned) {
|
|
13
|
+
warned = true
|
|
14
|
+
ctx.logger.warn(`shell-select: 宿主 assemble 链值缺 tools/sections 数组,pwsh 投影停用(宿主 ${'system-prompt'} 契约变更,登记 dsh-api-alignment)`)
|
|
15
|
+
}
|
|
16
|
+
return result
|
|
17
|
+
}
|
|
8
18
|
let sections = result.sections
|
|
9
19
|
const sdk = sections.find((section) => section.name === 'tools:sdk' && section.text.trim() !== '')
|
|
10
20
|
if (sdk !== undefined) {
|
|
@@ -4,7 +4,7 @@ import { test } from 'node:test'
|
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
5
5
|
import { installGuard } from '../src/guard.js'
|
|
6
6
|
import { MAIN_ROW_ID, OFFICIAL_ROW_IDS } from '../src/guard-state.mjs'
|
|
7
|
-
import { shellSelectApplyState } from '../src/apply-state.mjs'
|
|
7
|
+
import { shellSelectApplyState, shellSelectExecutorFiber } from '../src/apply-state.mjs'
|
|
8
8
|
|
|
9
9
|
function row({ id, disabled = false, running = false }) {
|
|
10
10
|
return {
|
|
@@ -158,3 +158,79 @@ test('模块归一:两形态皆无的官方包按预载失败处理,停用自愈
|
|
|
158
158
|
assert.equal(mounts.length, 0)
|
|
159
159
|
assert.ok(log.some(([level, msg]) => level === 'warn' && msg.includes('停用')))
|
|
160
160
|
})
|
|
161
|
+
|
|
162
|
+
// HOST-022(0.2.2 写事务重评 !!js 表达式):官方行翻回启用且主行执行器在场
|
|
163
|
+
// (句柄在场),guard 卸执行器让位;句柄空缺(guard 代挂态)零动作。
|
|
164
|
+
test('官方行翻回启用+执行器在场:dispose 句柄让位', async () => {
|
|
165
|
+
const rows = [
|
|
166
|
+
row({ id: MAIN_ROW_ID, disabled: false, running: true }),
|
|
167
|
+
row({ id: 'tool-pwsh', disabled: true }),
|
|
168
|
+
row({ id: 'pwsh-sandbox', disabled: false }),
|
|
169
|
+
]
|
|
170
|
+
const { ctx, log, listeners } = stubCtx({ rows })
|
|
171
|
+
await installGuard(ctx, { importOfficial: makeImport(), delay: async () => {}, sweepIntervalMs: 3600 * 1000 })
|
|
172
|
+
const patchContext = listeners.get('loader/patch-context')
|
|
173
|
+
let disposed = 0
|
|
174
|
+
shellSelectExecutorFiber.current = { dispose: () => { disposed += 1; return Promise.resolve() } }
|
|
175
|
+
try {
|
|
176
|
+
await patchContext(rows[2], () => 'passed')
|
|
177
|
+
assert.equal(disposed, 1)
|
|
178
|
+
assert.equal(shellSelectExecutorFiber.current, undefined)
|
|
179
|
+
assert.ok(log.some(([level, msg]) => level === 'warn' && msg.includes('让位')))
|
|
180
|
+
} finally {
|
|
181
|
+
shellSelectExecutorFiber.current = undefined
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('官方行保持禁用态 patch:零动作', async () => {
|
|
186
|
+
const rows = [
|
|
187
|
+
row({ id: MAIN_ROW_ID, disabled: false, running: true }),
|
|
188
|
+
row({ id: 'tool-pwsh', disabled: true }),
|
|
189
|
+
row({ id: 'pwsh-sandbox', disabled: true }),
|
|
190
|
+
]
|
|
191
|
+
const { ctx, listeners } = stubCtx({ rows })
|
|
192
|
+
await installGuard(ctx, { importOfficial: makeImport(), delay: async () => {}, sweepIntervalMs: 3600 * 1000 })
|
|
193
|
+
const patchContext = listeners.get('loader/patch-context')
|
|
194
|
+
let disposed = 0
|
|
195
|
+
shellSelectExecutorFiber.current = { dispose: () => { disposed += 1; return Promise.resolve() } }
|
|
196
|
+
try {
|
|
197
|
+
await patchContext(rows[1], () => 'passed')
|
|
198
|
+
assert.equal(disposed, 0)
|
|
199
|
+
assert.notEqual(shellSelectExecutorFiber.current, undefined)
|
|
200
|
+
} finally {
|
|
201
|
+
shellSelectExecutorFiber.current = undefined
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('句柄空缺(guard 代挂态):官方行翻回零动作', async () => {
|
|
206
|
+
const rows = [
|
|
207
|
+
row({ id: MAIN_ROW_ID, disabled: true }),
|
|
208
|
+
row({ id: 'tool-pwsh', disabled: true }),
|
|
209
|
+
row({ id: 'pwsh-sandbox', disabled: false }),
|
|
210
|
+
]
|
|
211
|
+
const { ctx, listeners } = stubCtx({ rows })
|
|
212
|
+
await installGuard(ctx, { importOfficial: makeImport(), delay: async () => {}, sweepIntervalMs: 3600 * 1000 })
|
|
213
|
+
const patchContext = listeners.get('loader/patch-context')
|
|
214
|
+
await patchContext(rows[2], () => 'passed')
|
|
215
|
+
assert.equal(shellSelectExecutorFiber.current, undefined)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('官方行求值抛错(disabledUnreliable):零动作安全向', async () => {
|
|
219
|
+
const rows = [
|
|
220
|
+
row({ id: MAIN_ROW_ID, disabled: false, running: true }),
|
|
221
|
+
row({ id: 'tool-pwsh', disabled: true }),
|
|
222
|
+
row({ id: 'pwsh-sandbox', disabled: true }),
|
|
223
|
+
]
|
|
224
|
+
const { ctx, listeners } = stubCtx({ rows })
|
|
225
|
+
await installGuard(ctx, { importOfficial: makeImport(), delay: async () => {}, sweepIntervalMs: 3600 * 1000 })
|
|
226
|
+
const patchContext = listeners.get('loader/patch-context')
|
|
227
|
+
let disposed = 0
|
|
228
|
+
shellSelectExecutorFiber.current = { dispose: () => { disposed += 1; return Promise.resolve() } }
|
|
229
|
+
try {
|
|
230
|
+
const hostile = { options: { id: 'pwsh-sandbox' }, get disabled() { throw new Error('broken') }, fiber: undefined }
|
|
231
|
+
await patchContext(hostile, () => 'passed')
|
|
232
|
+
assert.equal(disposed, 0)
|
|
233
|
+
} finally {
|
|
234
|
+
shellSelectExecutorFiber.current = undefined
|
|
235
|
+
}
|
|
236
|
+
})
|
|
@@ -118,3 +118,32 @@ test('SDK 投影不可用时整体不改并告警', async () => {
|
|
|
118
118
|
assert.equal(await handlers[0](final, {}, async () => final), final)
|
|
119
119
|
assert.equal(warnings.length, 1)
|
|
120
120
|
})
|
|
121
|
+
|
|
122
|
+
// Given 0.2.2 宿主 assemble 把 tools 移到 waterfall 之后合并,链值只含 sections/contexts/variables;
|
|
123
|
+
// When 投影读到链值缺 tools 数组;Then 原样透传不抛错,一次性告警登记契约变更(回合管线不崩)。
|
|
124
|
+
test('链值缺 tools 数组(0.2.2 契约)时透传且告警', async () => {
|
|
125
|
+
const handlers = []
|
|
126
|
+
const warnings = []
|
|
127
|
+
const ctx = { on: (name, handler) => { handlers.push(handler); return () => {} }, tools: { get: () => ({}) }, logger: { warn: (msg) => warnings.push(msg) } }
|
|
128
|
+
projection.mountShellPromptProjection(ctx)
|
|
129
|
+
const chainValue = { sections: [{ name: 'tool:pwsh' }, { name: 'policy' }], contexts: [], variables: {} }
|
|
130
|
+
const result = await handlers[0]({}, { scope: 'scope' }, async () => chainValue)
|
|
131
|
+
assert.equal(result, chainValue)
|
|
132
|
+
assert.deepEqual(result.sections.map((s) => s.name), ['tool:pwsh', 'policy'])
|
|
133
|
+
assert.equal(warnings.length, 1)
|
|
134
|
+
// 第二次组装不再重复告警
|
|
135
|
+
await handlers[0]({}, { scope: 'scope' }, async () => chainValue)
|
|
136
|
+
assert.equal(warnings.length, 1)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Given sections 缺席而 tools 在场的畸形链值;When 投影;Then 同样透传告警,不半改。
|
|
140
|
+
test('链值缺 sections 数组时同样透传告警', async () => {
|
|
141
|
+
const handlers = []
|
|
142
|
+
const warnings = []
|
|
143
|
+
const ctx = { on: (name, handler) => { handlers.push(handler); return () => {} }, tools: { get: () => ({}) }, logger: { warn: (msg) => warnings.push(msg) } }
|
|
144
|
+
projection.mountShellPromptProjection(ctx)
|
|
145
|
+
const chainValue = { tools: [{ name: 'pwsh' }] }
|
|
146
|
+
const result = await handlers[0]({}, { scope: 'scope' }, async () => chainValue)
|
|
147
|
+
assert.equal(result, chainValue)
|
|
148
|
+
assert.equal(warnings.length, 1)
|
|
149
|
+
})
|