@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 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.7",
4
+ "version": "0.4.9",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
@@ -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
- * 接管回滚。快照的陈旧由 apply 里的 loader/volatile-update 监听补偿:
114
- * volatile-only 变更宿主换值后发事件不重挂行,监听器现取 ref 解包成新
115
- * 快照经 ctx.shell.refresh 换源并重建工具。
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
- ctx.logger.warn('shell-select: 官方行退场超时,放弃接管(官方 fiber 仍在服务);重启后由行树声明态收敛')
151
- recordTakeover({ event: 'runtime 退场超时,放弃挂载' })
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
+ })