dsh-gitbash-shell 0.39.1 → 0.39.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/CHANGELOG.md +38 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -3
- package/src/bash-tool.js +26 -15
- package/src/client.js +36 -25
- package/src/compositions.js +11 -7
- package/src/fastctx-bridge.js +78 -31
- package/src/index.js +163 -141
- package/src/inline-tools.js +112 -49
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,44 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.39.2 — 2026-10-13
|
|
7
|
+
|
|
8
|
+
**类型**:fix(route B 工具/预设生命周期、动态配置与客户端回归) / test。
|
|
9
|
+
|
|
10
|
+
- **复现与裁定**:两名 Sol 分区审查、Grok 交叉反证与主代理隔离复验确认了工具面、生命周期、
|
|
11
|
+
组合、代码扫描和客户端状态转换的问题;普通子 scope 的原始冲突仍由 v0.39.1 的 `shell` 隔离修复。
|
|
12
|
+
FastCtx 全文路径回显是既定策略,不误当文件工具替换问题;minimal 的 persistent PTY 不与全工具
|
|
13
|
+
变体的执行器来源混为一谈。只修既有功能,不新增功能、改 root shell 或市场 patch。
|
|
14
|
+
- **变体接线**:Windows 全工具 Git Bash 变体不再启用依赖 root PowerShell 的官方 `tool-bash`;
|
|
15
|
+
保留官方 pwsh 面,经同一行内补位通道接入私有 Git Bash 工具。minimal 的 persistent 通道保持。
|
|
16
|
+
- **工具生命周期**:存活 agent 的 `pwshTool` 双向翻转同步解除/重建自身的 restrict;异步导入/
|
|
17
|
+
挂载在插件或 agent 退役后不再迟到注册;jobs 服务卸载按真实 Cordis 活跃状态恢复前台 bash。
|
|
18
|
+
- **降级分支**:plain definition 的 required 标志递归转换为合法 JSON Schema;升级助手不可用
|
|
19
|
+
时仍解析并传递每会话 standing policy,不错误回退部署默认。两项为模块解析降级的受控复现,
|
|
20
|
+
不宣称本机生产已发生导入失败,也不改变 Windows 受限模式明示 unconfined 的既定取舍。
|
|
21
|
+
- **预设与默认值**:串行 reconcile 退役时关闭新任务、清理晚到 register 并等待 unregister;
|
|
22
|
+
动态关闭变体在退役前迁回部署默认,避免后续新会话引用不存在的 preset。既有钉住会话不迁移。
|
|
23
|
+
- **路径与终端**:run_code 扫描辨认关键字后的正则并在不确定时整份 no-op,不改正则数据;
|
|
24
|
+
FastCtx 入/出参获得 execution 上下文并服从总方言、委托与虚拟挂载开关;better-sidebar 的
|
|
25
|
+
adoptSidebar 动态翻转恢复接管前值并保护接管后的用户手选,不改变官方 autoTerminalShell
|
|
26
|
+
关后只停止未来写入的独立语义。
|
|
27
|
+
- **FastCtx 错误**:子进程/写管道 error 进入初始化 Promise,失败清理 pending 计时器与连接;
|
|
28
|
+
MCP `isError:true` 按官方桥约定抛错,不再把失败包装成宿主成功 value。
|
|
29
|
+
发现与连接启动采用 single-flight,拥有在途连接并在卸载时取消/等待,不让并发 agent 的发现流程
|
|
30
|
+
覆盖已在使用的 lazy client。该丢失所有权链已用受控 MCP 传输复现,并新增关闭/晚注册回归。
|
|
31
|
+
- **客户端**:设置卡所有 Hooks 在 readiness early-return 前调用;工具链进度轮询在网络/HTTP
|
|
32
|
+
暂态失败后重试,卸载或失效时取消,不因单次请求失败永久卡在忙碌状态。
|
|
33
|
+
- **回归入口**:新增可移植客户端/桥/运行时/宿主回归与实际 Core preview 集成用例,接入现有
|
|
34
|
+
`npm test`/`npm run test:preview`;不新增依赖、不跳过旧断言或 Node 22 矩阵。对齐上游 v0.39.1
|
|
35
|
+
(`521be01`)后完整测试 **156/156**、实际 Core 无头 preview **14/14**、打包 dry-run 和 diff 检查
|
|
36
|
+
通过。新增执行级回归保留 #21 的 real-array glob 契约与 stringified-array 响亮拒绝;不改已发布
|
|
37
|
+
版本号、历史记录或 npm 发布流程。Grok 对修复 diff 的只读反证未发现确定遗漏。
|
|
38
|
+
首轮全量检查发现三条旧源码正则仍锁旧实现,改为对应新保护条件并补同值/手选行为测试,没有删掉
|
|
39
|
+
保护断言。原 [Issue #19](https://github.com/KannaKuron/dsh-gitbash-shell/issues/19) 和
|
|
40
|
+
[PR #20](https://github.com/KannaKuron/dsh-gitbash-shell/pull/20) 已完成;这些后续修复由新 PR 承载,
|
|
41
|
+
Windows/Linux × Node 22/24 的远端结果见其检查。preview 不等同于完整 Electron GUI 或真 MCP
|
|
42
|
+
exe 并发验收,模块解析降级使用受控夹具复现。
|
|
43
|
+
|
|
6
44
|
## v0.39.1 — 2026-10-13
|
|
7
45
|
|
|
8
46
|
**类型**:fix(route B 私有执行器的真实服务隔离与读取授权) / test / ci。
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:自带 bash 工具(私有 Git Bash 执行器实例,官方 PowerShell 执行器原样保留),官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0 起;v0.39.0 起不再替换宿主执行器),四个 *·Git Bash 变体 preset 可开关;官方 pwsh 工具默认隐藏、开关可见(真 PowerShell);附带 gb_* FastCtx 文件工具,检测到 dsh-ops 自动让位。联动:dsh-ptc-cordis-preset(PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端标签自动切 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
4
4
|
"description": "Windows 上给 agent 一个真正的 Git Bash 命令工具:私有 Git Bash 执行器实例(官方 PowerShell 执行器原样保留),统一 MSYS 盘根路径方言,官方 pwsh 工具默认隐藏可开关,物化四个内置预设的 Git Bash 变体,侧栏终端接管可开关。· Give agents a real Git Bash tool on Windows: a private Git Bash executor instance (the official PowerShell executor stays untouched), a unified MSYS drive-root path dialect, an opt-in official pwsh tool, and four Git Bash preset variants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"icon.svg"
|
|
53
53
|
],
|
|
54
54
|
"scripts": {
|
|
55
|
-
"test": "node --test tests/smoke.mjs tests/private-executor.mjs",
|
|
56
|
-
"test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs",
|
|
55
|
+
"test": "node --test tests/smoke.mjs tests/private-executor.mjs tests/client-regressions.mjs tests/bridge-regressions.mjs tests/runtime-regressions.mjs tests/host-regressions.mjs",
|
|
56
|
+
"test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs tests/bridge-preview.mjs tests/runtime-preview.mjs tests/host-preview.mjs",
|
|
57
57
|
"version": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dsh.plugin.json','utf8'));const v=JSON.parse(fs.readFileSync('package.json','utf8')).version;m.version=v;fs.writeFileSync('dsh.plugin.json',JSON.stringify(m,null,2)+'\\n')\" && git add dsh.plugin.json"
|
|
58
58
|
},
|
|
59
59
|
"keywords": [
|
package/src/bash-tool.js
CHANGED
|
@@ -248,20 +248,29 @@ function outputSchemaSpec() {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/**
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
out.properties[key] =
|
|
251
|
+
/** Convert key-level spec flags at every depth, including union branches. */
|
|
252
|
+
function schemaSpecToJson(spec) {
|
|
253
|
+
const { required: marker, ...out } = spec
|
|
254
|
+
if (Array.isArray(marker)) out.required = [...marker]
|
|
255
|
+
if (spec.properties !== undefined) {
|
|
256
|
+
const required = Array.isArray(marker) ? [...marker] : []
|
|
257
|
+
out.properties = {}
|
|
258
|
+
for (const [key, value] of Object.entries(spec.properties)) {
|
|
259
|
+
if (value.required === true && !required.includes(key)) required.push(key)
|
|
260
|
+
out.properties[key] = schemaSpecToJson(value)
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
if (required.length > 0) out.required = required
|
|
263
263
|
}
|
|
264
|
-
|
|
264
|
+
for (const key of ['oneOf', 'anyOf', 'allOf']) {
|
|
265
|
+
if (Array.isArray(spec[key])) out[key] = spec[key].map(schemaSpecToJson)
|
|
266
|
+
}
|
|
267
|
+
if (spec.items !== undefined) out.items = schemaSpecToJson(spec.items)
|
|
268
|
+
return out
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Plain JSON-schema variant of the same union (fallback when defineTool is absent). */
|
|
272
|
+
function outputSchemaJson() {
|
|
273
|
+
return schemaSpecToJson(outputSchemaSpec())
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
/** The official args validation (index.ts:73). */
|
|
@@ -320,8 +329,9 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
|
|
|
320
329
|
const defaultMode = executor.sandboxMode
|
|
321
330
|
const escalationReady = defaultMode !== undefined && typeof approveEscalation === 'function'
|
|
322
331
|
const escalationModes = escalationReady ? ESCALATION_TARGETS : []
|
|
332
|
+
// Missing approval helpers narrow escalation, not the session's standing policy.
|
|
323
333
|
const resolveSandboxPolicy = (exec) => {
|
|
324
|
-
if (
|
|
334
|
+
if (defaultMode === undefined) return undefined
|
|
325
335
|
const policy = ctx.get('sandboxPolicy')
|
|
326
336
|
return policy ? policy.resolve(exec?.agent === undefined ? {} : { session: exec.agent.session }) : undefined
|
|
327
337
|
}
|
|
@@ -548,7 +558,7 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
|
|
|
548
558
|
// keys reach execute too, where enableRunInBackground enforces them
|
|
549
559
|
// ("Undeclared keys are allowed, so schema omission also needs
|
|
550
560
|
// enforcement", index.ts:498).
|
|
551
|
-
parameters: { type: 'object', properties: parametersSpec()
|
|
561
|
+
parameters: schemaSpecToJson({ type: 'object', properties: parametersSpec() }),
|
|
552
562
|
output: {
|
|
553
563
|
schema: outputSchemaJson(),
|
|
554
564
|
render: (_args, value) => [{
|
|
@@ -580,7 +590,8 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
|
|
|
580
590
|
jobCtx.effect(() => () => {
|
|
581
591
|
unregister()
|
|
582
592
|
try {
|
|
583
|
-
|
|
593
|
+
// Cordis FiberState.ACTIVE is the numeric enum member 2.
|
|
594
|
+
if (ctx.fiber?.state === 2) foregroundOnly = ctx.tools.register(bashTool(undefined))
|
|
584
595
|
} catch { /* the scope is going away */ }
|
|
585
596
|
})
|
|
586
597
|
})
|
package/src/client.js
CHANGED
|
@@ -2063,6 +2063,42 @@ window.__ModuleLoader__.load({
|
|
|
2063
2063
|
if (scope && typeof scope.getSnapshot === "function") snap = scope.getSnapshot();
|
|
2064
2064
|
} catch (error_) { /* keep unavailable */ }
|
|
2065
2065
|
|
|
2066
|
+
var pageView = props.view === "page";
|
|
2067
|
+
|
|
2068
|
+
// Hooks run for every snapshot state; readiness only gates their work.
|
|
2069
|
+
useEffect(function () {
|
|
2070
|
+
if (snap.status !== "ready" || (!open && !pageView)) return undefined;
|
|
2071
|
+
if (toolsLoaded || toolBusy) return undefined;
|
|
2072
|
+
loadTools();
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}, [open, pageView, snap.status]);
|
|
2075
|
+
|
|
2076
|
+
/* Poll ONLY while a job is unfinished, and through the cheap ?job=1
|
|
2077
|
+
form. A transient failure retries without discarding progress. */
|
|
2078
|
+
useEffect(function () {
|
|
2079
|
+
if (snap.status !== "ready" || toolJob === null || toolJob.finished) return undefined;
|
|
2080
|
+
var alive = true;
|
|
2081
|
+
var timer;
|
|
2082
|
+
function schedule() {
|
|
2083
|
+
timer = setTimeout(function () {
|
|
2084
|
+
fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
|
|
2085
|
+
.then(function (res) {
|
|
2086
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
2087
|
+
return res.json();
|
|
2088
|
+
})
|
|
2089
|
+
.then(function (data) {
|
|
2090
|
+
if (!alive) return;
|
|
2091
|
+
var next = data !== null && typeof data === "object" && data.job ? data.job : null;
|
|
2092
|
+
setToolJob(next);
|
|
2093
|
+
if (next !== null && next.finished) loadTools();
|
|
2094
|
+
})
|
|
2095
|
+
.catch(function () { if (alive) schedule(); });
|
|
2096
|
+
}, 1200);
|
|
2097
|
+
}
|
|
2098
|
+
schedule();
|
|
2099
|
+
return function () { alive = false; clearTimeout(timer); };
|
|
2100
|
+
}, [toolJob, snap.status]);
|
|
2101
|
+
|
|
2066
2102
|
if (snap.status !== "ready") return null;
|
|
2067
2103
|
|
|
2068
2104
|
var value = snap.value || {};
|
|
@@ -2135,7 +2171,6 @@ window.__ModuleLoader__.load({
|
|
|
2135
2171
|
// dsh 0.1.6-alpha.2: the Plugins page renders this card through the
|
|
2136
2172
|
// `plugins.bundle.config` slot with view="page" — the page draws the
|
|
2137
2173
|
// title itself; the collapsible shell only shows on the popup seat.
|
|
2138
|
-
var pageView = props.view === "page";
|
|
2139
2174
|
|
|
2140
2175
|
var mounts = snap.value.virtualMounts !== false;
|
|
2141
2176
|
var errs = snap.value.errorDialect !== false;
|
|
@@ -2469,30 +2504,6 @@ window.__ModuleLoader__.load({
|
|
|
2469
2504
|
.then(function () { setToolBusy(false); });
|
|
2470
2505
|
}
|
|
2471
2506
|
|
|
2472
|
-
useEffect(function () {
|
|
2473
|
-
if (!open && !pageView) return undefined;
|
|
2474
|
-
if (toolsLoaded || toolBusy) return undefined;
|
|
2475
|
-
loadTools();
|
|
2476
|
-
return undefined;
|
|
2477
|
-
}, [open]);
|
|
2478
|
-
|
|
2479
|
-
/* Poll ONLY while a job is unfinished, and through the cheap ?job=1
|
|
2480
|
-
form: a full probe re-runs three winget commands per second. */
|
|
2481
|
-
useEffect(function () {
|
|
2482
|
-
if (toolJob === null || toolJob.finished) return undefined;
|
|
2483
|
-
var timer = setTimeout(function () {
|
|
2484
|
-
fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
|
|
2485
|
-
.then(function (res) { return res.json(); })
|
|
2486
|
-
.then(function (data) {
|
|
2487
|
-
var next = data !== null && typeof data === "object" && data.job ? data.job : null;
|
|
2488
|
-
setToolJob(next);
|
|
2489
|
-
if (next !== null && next.finished) loadTools();
|
|
2490
|
-
})
|
|
2491
|
-
.catch(function () { /* keep the last known progress */ });
|
|
2492
|
-
}, 1200);
|
|
2493
|
-
return function () { clearTimeout(timer); };
|
|
2494
|
-
}, [toolJob]);
|
|
2495
|
-
|
|
2496
2507
|
var toolsSection = fold("sec.tools",
|
|
2497
2508
|
toolsData === null && toolErr === "" ? E("p", { className: "gb-hint" }, t("tools.loading")) : null,
|
|
2498
2509
|
toolErr !== "" ? E("p", { className: "gb-error" }, toolErr) : null,
|
package/src/compositions.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Every variant mirrors the official 0.1.7 preset it replaces (packages/
|
|
11
11
|
* bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Git Bash routing: full variants keep the official platform-gated shell
|
|
13
|
+
* rows, and the inline patch adds OUR bash over a private executor on Windows.
|
|
14
|
+
* The minimal variant instead keeps its persistent Git Bash terminal rows.
|
|
15
|
+
* On non-Windows hosts the official stack stays native. A conditional delta exists while
|
|
16
16
|
* dsh-ptc-cordis-preset reports its experimental-CPython backend ACTIVE: the
|
|
17
17
|
* two workflow rows follow the official Python composition and go off (see
|
|
18
18
|
* `pythonActive` below). While that backend is off — the default, and also
|
|
@@ -56,7 +56,8 @@ const planSectionFor = (hostEra021) => hostEra021
|
|
|
56
56
|
* @param {'standard'|'ptc'|'cordis'} input.kind - which official preset this
|
|
57
57
|
* variant mirrors (workflow rows follow it: enabled on standard/cordis,
|
|
58
58
|
* disabled on ptc).
|
|
59
|
-
* @param {boolean} input.gitBash -
|
|
59
|
+
* @param {boolean} input.gitBash - retained for composition-call compatibility;
|
|
60
|
+
* full variants obtain Git Bash from the per-agent inline patch on Windows.
|
|
60
61
|
* @param {string|undefined} input.skillsDir - Creation authoring skills
|
|
61
62
|
* directory (cordis variant only; resolved beside the agent-preset package).
|
|
62
63
|
* @param {boolean} [input.pythonActive] - whether the experimental CPython
|
|
@@ -75,8 +76,11 @@ const planSectionFor = (hostEra021) => hostEra021
|
|
|
75
76
|
*/
|
|
76
77
|
export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hostExtras = {} }) {
|
|
77
78
|
const win = typeof process !== 'undefined' && process.platform === 'win32'
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Route B preserves the root PowerShell executor. On Windows the official
|
|
80
|
+
// tool-bash would execute through that executor, so full variants retain the
|
|
81
|
+
// official pwsh face and let the same per-agent inline patch add OUR bash.
|
|
82
|
+
const bashDisabled = win
|
|
83
|
+
const pwshDisabled = !win
|
|
80
84
|
const workflowOn = kind !== 'ptc' && pythonActive !== true
|
|
81
85
|
// dsh 0.2.1 added two rows to every full-tool official preset — time-context
|
|
82
86
|
// (durable clock readings) and tool-schedule (the reminder tools) — plus a
|
package/src/fastctx-bridge.js
CHANGED
|
@@ -106,20 +106,21 @@ export class McpStdio {
|
|
|
106
106
|
this.child.stderr.on('data', (chunk) => {
|
|
107
107
|
this.stderrTail = (this.stderrTail + chunk).slice(-2000)
|
|
108
108
|
})
|
|
109
|
-
this.child.on('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
this.child.on('error', (error) => this.#fail(error))
|
|
110
|
+
this.child.stdin.on('error', (error) => this.#fail(error))
|
|
111
|
+
this.child.on('exit', (code) => this.#fail(new Error(`the FastCtx server exited (code ${code ?? 'null'})`)))
|
|
112
|
+
try {
|
|
113
|
+
await this.#request('initialize', {
|
|
114
|
+
protocolVersion: '2024-11-05',
|
|
115
|
+
capabilities: {},
|
|
116
|
+
clientInfo: { name: 'dsh-gitbash-shell', version: '0.35.0' },
|
|
117
|
+
})
|
|
118
|
+
this.#notify('notifications/initialized')
|
|
119
|
+
return this
|
|
120
|
+
} catch (error) {
|
|
121
|
+
await this.close()
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
/** List the server's tools: [{ name, description, inputSchema }]. */
|
|
@@ -140,15 +141,21 @@ export class McpStdio {
|
|
|
140
141
|
this.dead = true
|
|
141
142
|
const child = this.child
|
|
142
143
|
this.child = undefined
|
|
143
|
-
|
|
144
|
-
reject(new Error('the FastCtx bridge was stopped'))
|
|
145
|
-
}
|
|
146
|
-
this.pending.clear()
|
|
144
|
+
this.#fail(new Error('the FastCtx bridge was stopped'))
|
|
147
145
|
if (child && child.exitCode === null && !child.killed) {
|
|
148
146
|
try { child.kill() } catch { /* already gone */ }
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
150
|
+
#fail(error) {
|
|
151
|
+
this.dead = true
|
|
152
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
153
|
+
clearTimeout(timer)
|
|
154
|
+
reject(error)
|
|
155
|
+
}
|
|
156
|
+
this.pending.clear()
|
|
157
|
+
}
|
|
158
|
+
|
|
152
159
|
#onChunk(chunk) {
|
|
153
160
|
this.buffer += chunk
|
|
154
161
|
let at = this.buffer.indexOf('\n')
|
|
@@ -310,6 +317,10 @@ export function mountFastctx(ctx, deps) {
|
|
|
310
317
|
let client = undefined
|
|
311
318
|
let decided = false
|
|
312
319
|
let withdrawnByOps = false
|
|
320
|
+
let closed = false
|
|
321
|
+
let publishing
|
|
322
|
+
let connecting
|
|
323
|
+
const starting = new Set()
|
|
313
324
|
|
|
314
325
|
const unpublish = (reason) => {
|
|
315
326
|
if (disposer) {
|
|
@@ -320,16 +331,34 @@ export function mountFastctx(ctx, deps) {
|
|
|
320
331
|
}
|
|
321
332
|
|
|
322
333
|
const callWithReconnect = async (rawName, args) => {
|
|
334
|
+
if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
|
|
323
335
|
if (client === undefined || client.isDead) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
336
|
+
if (!connecting) {
|
|
337
|
+
connecting = (async () => {
|
|
338
|
+
const { file } = resolveFastctx(ctx, createRequireFrom)
|
|
339
|
+
if (file === '') throw new Error('the FastCtx payload is not installed; repair the profile dependencies (frozen install) and retry')
|
|
340
|
+
const fresh = new McpStdio({ file, log })
|
|
341
|
+
starting.add(fresh)
|
|
342
|
+
try {
|
|
343
|
+
await fresh.start()
|
|
344
|
+
if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
|
|
345
|
+
client = fresh
|
|
346
|
+
} catch (error) {
|
|
347
|
+
await fresh.close()
|
|
348
|
+
throw error
|
|
349
|
+
} finally {
|
|
350
|
+
starting.delete(fresh)
|
|
351
|
+
}
|
|
352
|
+
})().finally(() => { connecting = undefined })
|
|
353
|
+
}
|
|
354
|
+
await connecting
|
|
327
355
|
}
|
|
356
|
+
if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
|
|
328
357
|
return client.callTool(rawName, args)
|
|
329
358
|
}
|
|
330
359
|
|
|
331
|
-
const
|
|
332
|
-
if (disposer || decided && withdrawnByOps) return
|
|
360
|
+
const publishOnce = async () => {
|
|
361
|
+
if (closed || disposer || decided && withdrawnByOps) return
|
|
333
362
|
const registry = tools(ctx)
|
|
334
363
|
if (!registry || typeof registry.register !== 'function') return
|
|
335
364
|
const { file, source } = resolveFastctx(ctx, createRequireFrom)
|
|
@@ -340,6 +369,7 @@ export function mountFastctx(ctx, deps) {
|
|
|
340
369
|
return
|
|
341
370
|
}
|
|
342
371
|
const fresh = new McpStdio({ file, log })
|
|
372
|
+
starting.add(fresh)
|
|
343
373
|
let toolList
|
|
344
374
|
try {
|
|
345
375
|
await fresh.start()
|
|
@@ -350,9 +380,12 @@ export function mountFastctx(ctx, deps) {
|
|
|
350
380
|
decided = true
|
|
351
381
|
withdrawnByOps = true
|
|
352
382
|
return
|
|
383
|
+
} finally {
|
|
384
|
+
starting.delete(fresh)
|
|
353
385
|
}
|
|
354
386
|
await fresh.close()
|
|
355
|
-
|
|
387
|
+
if (closed || withdrawnByOps) return
|
|
388
|
+
// Discovery never overwrites the independently owned lazy call client.
|
|
356
389
|
const wanted = toolList.filter((tool) => FASTCTX_FILE_TOOLS.includes(String(tool?.name)))
|
|
357
390
|
const registered = []
|
|
358
391
|
const registeredNames = []
|
|
@@ -386,16 +419,20 @@ export function mountFastctx(ctx, deps) {
|
|
|
386
419
|
schema: TOOL_OUTPUT_SCHEMA,
|
|
387
420
|
render: (_args, value) => renderTextContent(value),
|
|
388
421
|
},
|
|
389
|
-
async execute(args) {
|
|
390
|
-
//
|
|
391
|
-
//
|
|
422
|
+
async execute(args, execution) {
|
|
423
|
+
// Preserve v0.39.1 / #21: glob accepts real string arrays,
|
|
424
|
+
// while stringified arrays and malformed patterns fail loudly.
|
|
392
425
|
if (args !== null && typeof args === 'object' && 'pattern' in args) {
|
|
393
426
|
const invalid = validateFastctxPattern(rawName, args.pattern)
|
|
394
427
|
if (invalid) throw new Error(invalid)
|
|
395
428
|
}
|
|
396
|
-
const translated = translateFastctxArgs(args, msysToWin)
|
|
429
|
+
const translated = translateFastctxArgs(args, msysToWin ? (path) => msysToWin(path, execution) : undefined)
|
|
397
430
|
const result = await callWithReconnect(rawName, translated)
|
|
398
|
-
|
|
431
|
+
const text = renderFastctxResult(rawName, result, echo ? (value) => echo(value, execution) : undefined)
|
|
432
|
+
// The host treats a returned value as success; MCP failures must
|
|
433
|
+
// throw, just as its official MCP client does.
|
|
434
|
+
if (result?.isError === true) throw new Error(text)
|
|
435
|
+
return { content: [{ type: 'text', text }] }
|
|
399
436
|
},
|
|
400
437
|
})
|
|
401
438
|
if (typeof dispose === 'function') {
|
|
@@ -416,7 +453,13 @@ export function mountFastctx(ctx, deps) {
|
|
|
416
453
|
log(`FastCtx bridge active (${source}): ${registeredNames.join(', ')}`)
|
|
417
454
|
}
|
|
418
455
|
|
|
456
|
+
const publish = () => {
|
|
457
|
+
if (!publishing) publishing = publishOnce().finally(() => { publishing = undefined })
|
|
458
|
+
return publishing
|
|
459
|
+
}
|
|
460
|
+
|
|
419
461
|
const reconcile = (agent) => {
|
|
462
|
+
if (closed) return
|
|
420
463
|
if (!enabled()) { unpublish('FastCtx bridge disabled by the fastctxTools switch'); decided = true; withdrawnByOps = true; return }
|
|
421
464
|
if (decided && withdrawnByOps) return
|
|
422
465
|
let names = []
|
|
@@ -440,8 +483,12 @@ export function mountFastctx(ctx, deps) {
|
|
|
440
483
|
log(`FastCtx bridge wiring failed: ${error?.message ?? error}`)
|
|
441
484
|
return
|
|
442
485
|
}
|
|
443
|
-
ctx.effect(() => () => {
|
|
486
|
+
ctx.effect(() => async () => {
|
|
487
|
+
closed = true
|
|
444
488
|
unpublish()
|
|
445
|
-
|
|
489
|
+
const owned = new Set([...starting, client].filter(Boolean))
|
|
490
|
+
await Promise.allSettled([...owned].map((connection) => connection.close()))
|
|
491
|
+
await Promise.allSettled([publishing, connecting].filter(Boolean))
|
|
492
|
+
client = undefined
|
|
446
493
|
}, 'dsh-gitbash-shell: FastCtx bridge')
|
|
447
494
|
}
|
package/src/index.js
CHANGED
|
@@ -1168,7 +1168,13 @@ const CODE_LITERAL_SAFE = /^[A-Za-z0-9._@+~\-/ ]+$/
|
|
|
1168
1168
|
/** Previous significant character that lets a '/' start a regex literal. */
|
|
1169
1169
|
function regexCanStart(previous) {
|
|
1170
1170
|
if (previous === '') return true
|
|
1171
|
-
|
|
1171
|
+
if (['return', 'throw', 'yield', 'await', 'case', 'delete', 'void', 'typeof', 'in', 'instanceof', 'of', 'else', 'do', 'break', 'continue', 'debugger'].includes(previous)) return true
|
|
1172
|
+
return previous.length === 1 && '(,=:[!&|?{;+-*%<>~^'.includes(previous)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** Read an identifier as ONE token: keyword-led regexes are not division. */
|
|
1176
|
+
function codeWordAt(code, index) {
|
|
1177
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(code.slice(index))?.[0]
|
|
1172
1178
|
}
|
|
1173
1179
|
|
|
1174
1180
|
/** Index after the closing quote of the literal at `index`, or -1. */
|
|
@@ -1239,6 +1245,15 @@ function skipBracedExpression(code, index) {
|
|
|
1239
1245
|
last = 'x'
|
|
1240
1246
|
continue
|
|
1241
1247
|
}
|
|
1248
|
+
// A slash after a closing group may start a statement regex OR division.
|
|
1249
|
+
// Without a parser that distinction is uncertain: abort the WHOLE scan.
|
|
1250
|
+
if (c === '/' && (last === ')' || last === '}')) return -1
|
|
1251
|
+
const word = codeWordAt(code, i)
|
|
1252
|
+
if (word) {
|
|
1253
|
+
last = last === '.' ? 'x' : word
|
|
1254
|
+
i += word.length
|
|
1255
|
+
continue
|
|
1256
|
+
}
|
|
1242
1257
|
if (c === '/' && regexCanStart(last)) {
|
|
1243
1258
|
const end = skipRegexLiteral(code, i)
|
|
1244
1259
|
if (end === -1) return -1
|
|
@@ -1320,6 +1335,15 @@ export function scanCodeLiterals(code) {
|
|
|
1320
1335
|
last = 'x'
|
|
1321
1336
|
continue
|
|
1322
1337
|
}
|
|
1338
|
+
// A slash after a closing group may start a statement regex OR division.
|
|
1339
|
+
// Without a parser that distinction is uncertain: abort the WHOLE scan.
|
|
1340
|
+
if (c === '/' && (last === ')' || last === '}')) return null
|
|
1341
|
+
const word = codeWordAt(code, i)
|
|
1342
|
+
if (word) {
|
|
1343
|
+
last = last === '.' ? 'x' : word
|
|
1344
|
+
i += word.length
|
|
1345
|
+
continue
|
|
1346
|
+
}
|
|
1323
1347
|
if (c === '/' && regexCanStart(last)) {
|
|
1324
1348
|
const end = skipRegexLiteral(code, i)
|
|
1325
1349
|
if (end === -1) return null
|
|
@@ -1380,8 +1404,8 @@ export function rewriteCodePaths(code, env) {
|
|
|
1380
1404
|
// is written into, and a quote inside it (a home directory with an
|
|
1381
1405
|
// apostrophe) would end that literal early — refuse rather than corrupt.
|
|
1382
1406
|
const quote = code[span.start - 1]
|
|
1383
|
-
if (translated.indexOf(quote) >= 0)
|
|
1384
|
-
if (quote === '`' && translated.indexOf('${') >= 0)
|
|
1407
|
+
if (translated.indexOf(quote) >= 0) return code
|
|
1408
|
+
if (quote === '`' && translated.indexOf('${') >= 0) return code
|
|
1385
1409
|
const source = translated.replace(/\\/g, '\\\\')
|
|
1386
1410
|
out += code.slice(cursor, span.start) + source
|
|
1387
1411
|
cursor = span.end
|
|
@@ -1469,9 +1493,6 @@ const SIDEBAR_NS = 'dsh-better-sidebar'
|
|
|
1469
1493
|
const SIDEBAR_TRIES = 12
|
|
1470
1494
|
const SIDEBAR_RETRY_MS = 1500
|
|
1471
1495
|
|
|
1472
|
-
// Reconciler installed by adoptSidebarShell; a no-op until then.
|
|
1473
|
-
let reconcileSidebar = () => {}
|
|
1474
|
-
|
|
1475
1496
|
/**
|
|
1476
1497
|
* Reconcile the better-sidebar `terminalShell` takeover with the adoptSidebar
|
|
1477
1498
|
* setting (v0.15.0, default ON). ON writes our bashPath (remembering what was
|
|
@@ -1491,95 +1512,69 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
|
|
|
1491
1512
|
let previous = ''
|
|
1492
1513
|
let tried = 0
|
|
1493
1514
|
let timer = null
|
|
1494
|
-
|
|
1495
|
-
|
|
1515
|
+
let closed = false
|
|
1516
|
+
let serial = Promise.resolve()
|
|
1496
1517
|
const readShell = () => {
|
|
1497
1518
|
const settings = ctx.get('settings')
|
|
1498
1519
|
if (!settings || typeof settings.update !== 'function') return null
|
|
1499
1520
|
try {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1521
|
+
if (typeof settings.describe !== 'function') return null
|
|
1522
|
+
const entry = settings.describe().find((row) => row && row.ns === SIDEBAR_NS)
|
|
1523
|
+
const value = entry?.value
|
|
1524
|
+
return value && typeof value.terminalShell === 'string' ? value.terminalShell : ''
|
|
1525
|
+
} catch { return null }
|
|
1526
|
+
}
|
|
1527
|
+
const restore = async () => {
|
|
1528
|
+
if (!adopted) return
|
|
1529
|
+
const now = readShell()
|
|
1530
|
+
if (now === null) return
|
|
1531
|
+
// The user may have selected another shell since our takeover.
|
|
1532
|
+
if (now === bashPath) {
|
|
1533
|
+
await ctx.get('settings').update(SIDEBAR_NS, { terminalShell: previous })
|
|
1534
|
+
console.log(TAG + " better-sidebar shell restored ('" + previous + "')")
|
|
1511
1535
|
}
|
|
1536
|
+
adopted = false
|
|
1512
1537
|
}
|
|
1513
|
-
|
|
1514
|
-
reconcileSidebar = (desired) => {
|
|
1538
|
+
const reconcileSidebar = async (desired) => {
|
|
1515
1539
|
const current = readShell()
|
|
1516
1540
|
if (current === null) return false
|
|
1517
|
-
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
adopted = true
|
|
1524
|
-
console.log(
|
|
1525
|
-
`${TAG} dsh-better-sidebar terminal shell -> Git Bash (${bashPath})` +
|
|
1526
|
-
(was ? ` (took over from '${was}')` : ''),
|
|
1527
|
-
)
|
|
1528
|
-
})
|
|
1529
|
-
.catch((error) => {
|
|
1530
|
-
console.log(`${TAG} better-sidebar shell adoption unavailable: ${error?.message ?? error}`)
|
|
1531
|
-
})
|
|
1532
|
-
return true
|
|
1533
|
-
}
|
|
1534
|
-
if (desired && current === bashPath) {
|
|
1535
|
-
adopted = true
|
|
1536
|
-
return true
|
|
1537
|
-
}
|
|
1538
|
-
if (!desired && adopted && current === bashPath) {
|
|
1539
|
-
settings.update(SIDEBAR_NS, { terminalShell: previous })
|
|
1540
|
-
.then(() => {
|
|
1541
|
-
adopted = false
|
|
1542
|
-
console.log(`${TAG} dsh-better-sidebar terminal shell restored ('${previous || 'sidebar default'}') — adoptSidebar off`)
|
|
1543
|
-
})
|
|
1544
|
-
.catch(() => {})
|
|
1545
|
-
return true
|
|
1546
|
-
}
|
|
1541
|
+
if (!desired) { await restore(); return true }
|
|
1542
|
+
if (adopted || current === bashPath) return true
|
|
1543
|
+
previous = current
|
|
1544
|
+
await ctx.get('settings').update(SIDEBAR_NS, { terminalShell: bashPath })
|
|
1545
|
+
adopted = true
|
|
1546
|
+
console.log(TAG + ' dsh-better-sidebar terminal shell -> Git Bash (' + bashPath + ')')
|
|
1547
1547
|
return true
|
|
1548
1548
|
}
|
|
1549
|
-
|
|
1550
1549
|
const run = () => {
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
(
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
},
|
|
1581
|
-
'dsh-gitbash-shell: sidebar shell revert',
|
|
1582
|
-
)
|
|
1550
|
+
if (closed) return serial
|
|
1551
|
+
if (timer) { clearTimeout(timer); timer = null }
|
|
1552
|
+
serial = serial.then(async () => {
|
|
1553
|
+
if (closed) return
|
|
1554
|
+
tried += 1
|
|
1555
|
+
const ok = await reconcileSidebar(readAdopt())
|
|
1556
|
+
if (ok || closed) return
|
|
1557
|
+
if (tried < SIDEBAR_TRIES) timer = setTimeout(run, SIDEBAR_RETRY_MS)
|
|
1558
|
+
else console.log(TAG + ' better-sidebar shell adoption skipped: settings unavailable')
|
|
1559
|
+
}).catch((error) => {
|
|
1560
|
+
console.log(TAG + ' better-sidebar shell adoption unavailable: ' + (error?.message ?? error))
|
|
1561
|
+
})
|
|
1562
|
+
return serial
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
ctx.on('loader/volatile-update', (paths) => {
|
|
1566
|
+
if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && path[0] === 'adoptSidebar')) return
|
|
1567
|
+
tried = 0
|
|
1568
|
+
void run()
|
|
1569
|
+
})
|
|
1570
|
+
} catch { /* a diagnostic context may have no event service */ }
|
|
1571
|
+
ctx.effect(() => async () => {
|
|
1572
|
+
closed = true
|
|
1573
|
+
if (timer) clearTimeout(timer)
|
|
1574
|
+
await serial
|
|
1575
|
+
await restore()
|
|
1576
|
+
}, 'dsh-gitbash-shell: sidebar shell revert')
|
|
1577
|
+
return run()
|
|
1583
1578
|
}
|
|
1584
1579
|
|
|
1585
1580
|
// ── plugin ──────────────────────────────────────────────────────────────────
|
|
@@ -1759,6 +1754,38 @@ export function minimalPtyBashPath(readResolution) {
|
|
|
1759
1754
|
* failures stay visible through the roster's broken diagnostic instead of
|
|
1760
1755
|
* failing the boot.
|
|
1761
1756
|
*/
|
|
1757
|
+
async function migrateVariantDefault(ctx, readVariants) {
|
|
1758
|
+
if (readVariants() === true) return true
|
|
1759
|
+
const readDefault = () => { try { return ctx.agentPresets.defaultId } catch { return undefined } }
|
|
1760
|
+
const current = readDefault()
|
|
1761
|
+
if (typeof current !== 'string' || !PRESET_IDS.includes(current)) return true
|
|
1762
|
+
const target = VARIANT_DEFAULT_SUCCESSORS[current]
|
|
1763
|
+
try {
|
|
1764
|
+
const settings = ctx.get('settings')
|
|
1765
|
+
if (!settings || typeof settings.update !== 'function') return false
|
|
1766
|
+
const roster = await ctx.agentPresets.list()
|
|
1767
|
+
if (!Array.isArray(roster) || !roster.some((row) => row && row.id === target)) {
|
|
1768
|
+
console.warn(TAG + ' default-preset migration stood down: successor "' + target + '" is not registered. Set an OFFICIAL default or turn variantPresets on.')
|
|
1769
|
+
return false
|
|
1770
|
+
}
|
|
1771
|
+
// A roster read is asynchronous: never overwrite a newer user selection.
|
|
1772
|
+
if (readVariants() === true || readDefault() !== current) return true
|
|
1773
|
+
await settings.update('agent-preset-registry', { selectedDefault: target })
|
|
1774
|
+
const after = readDefault()
|
|
1775
|
+
if (after !== target) {
|
|
1776
|
+
console.warn(TAG + ' default-preset migration did not stick (read back "' + after + '"). Set an OFFICIAL default or turn variantPresets on.')
|
|
1777
|
+
return false
|
|
1778
|
+
}
|
|
1779
|
+
console.log(TAG + ' deployment default migrated: "' + current + '" -> "' + target
|
|
1780
|
+
+ '" (variantPresets is off). Pinned sessions are unchanged; agent-preset/locked still requires a NEW session.')
|
|
1781
|
+
return true
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
console.warn(TAG + ' default-preset migration failed: ' + (error?.message ?? error)
|
|
1784
|
+
+ '. Set an OFFICIAL default or turn variantPresets on; variants were not retired.')
|
|
1785
|
+
return false
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1762
1789
|
async function registerVariant(ctx, presetId, { gitBashActive, skillsDir, pythonActive, hostExtras = {}, readBashResolution }) {
|
|
1763
1790
|
const meta = PRESET_META[presetId]
|
|
1764
1791
|
if (!meta) return undefined
|
|
@@ -1811,6 +1838,17 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1811
1838
|
/** The CPython fact the LIVE registrations were built with. */
|
|
1812
1839
|
let registeredPython = false
|
|
1813
1840
|
let serial = Promise.resolve()
|
|
1841
|
+
let closed = false
|
|
1842
|
+
ctx.effect(() => async () => {
|
|
1843
|
+
closed = true
|
|
1844
|
+
await serial
|
|
1845
|
+
const failures = []
|
|
1846
|
+
for (const [presetId, off] of [...live]) {
|
|
1847
|
+
live.delete(presetId)
|
|
1848
|
+
try { await off() } catch (error) { failures.push(error) }
|
|
1849
|
+
}
|
|
1850
|
+
if (failures.length) throw new AggregateError(failures, 'Git Bash preset cleanup failed')
|
|
1851
|
+
}, 'dsh-gitbash-shell: declarative preset registrations')
|
|
1814
1852
|
|
|
1815
1853
|
// v0.35.0: the variant roster is opt-in (`variantPresets`, default OFF).
|
|
1816
1854
|
// OFF means an EMPTY desired set — the cleanup above still runs, so a
|
|
@@ -1820,8 +1858,16 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1820
1858
|
: [])
|
|
1821
1859
|
|
|
1822
1860
|
const reconcile = () => {
|
|
1861
|
+
if (closed) return serial
|
|
1823
1862
|
serial = serial.then(async () => {
|
|
1863
|
+
if (closed) return
|
|
1824
1864
|
const want = desired()
|
|
1865
|
+
const pythonActive = peerPython
|
|
1866
|
+
// Dynamic OFF repairs the deployment default BEFORE retiring its id.
|
|
1867
|
+
// Keep registrations if the official migration cannot be verified.
|
|
1868
|
+
if (live.size > 0 && readVariants() !== true
|
|
1869
|
+
&& !await migrateVariantDefault(ctx, readVariants)) return
|
|
1870
|
+
if (closed) return
|
|
1825
1871
|
// The peer's EFFECTIVE CPython backend rewrites the workflow rows of
|
|
1826
1872
|
// EVERY variant (the official Python composition disables them, and
|
|
1827
1873
|
// workflow-ptc refuses a non-TypeScript backend), so a flip re-registers
|
|
@@ -1829,14 +1875,16 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1829
1875
|
// as the backend swap itself: the peer's patch is evaluated at boot. A
|
|
1830
1876
|
// bare INTENT without the effective backend changes nothing here — the
|
|
1831
1877
|
// composition still runs Node, so the workflow rows must stay live.
|
|
1832
|
-
if (live.size > 0 && registeredPython !==
|
|
1878
|
+
if (live.size > 0 && registeredPython !== pythonActive) {
|
|
1833
1879
|
for (const [presetId, off] of [...live]) {
|
|
1834
1880
|
live.delete(presetId)
|
|
1835
1881
|
try {
|
|
1836
1882
|
await off()
|
|
1837
1883
|
console.log(TAG + " preset '" + presetId + "' retired (peer CPython backend changed; rows are rebuilt)")
|
|
1838
1884
|
} catch (error) {
|
|
1885
|
+
live.set(presetId, off)
|
|
1839
1886
|
console.log(TAG + " preset '" + presetId + "' retirement failed: " + (error && error.message ? error.message : error))
|
|
1887
|
+
return // never register replacements after an unverified retirement
|
|
1840
1888
|
}
|
|
1841
1889
|
}
|
|
1842
1890
|
}
|
|
@@ -1847,14 +1895,18 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1847
1895
|
await off()
|
|
1848
1896
|
console.log(TAG + " preset '" + presetId + "' retired (dsh-ptc-cordis-preset covers Creation mode on Git Bash)")
|
|
1849
1897
|
} catch (error) {
|
|
1898
|
+
live.set(presetId, off)
|
|
1850
1899
|
console.log(TAG + " preset '" + presetId + "' retirement failed: " + (error && error.message ? error.message : error))
|
|
1900
|
+
return
|
|
1851
1901
|
}
|
|
1852
1902
|
}
|
|
1853
1903
|
for (const presetId of want) {
|
|
1904
|
+
if (closed) break
|
|
1854
1905
|
if (live.has(presetId)) continue
|
|
1855
1906
|
try {
|
|
1856
|
-
const unregister = await registerVariant(ctx, presetId, { gitBashActive, skillsDir, pythonActive
|
|
1907
|
+
const unregister = await registerVariant(ctx, presetId, { gitBashActive, skillsDir, pythonActive, hostExtras, readBashResolution })
|
|
1857
1908
|
if (unregister) {
|
|
1909
|
+
if (closed) { await unregister(); break }
|
|
1858
1910
|
live.set(presetId, unregister)
|
|
1859
1911
|
console.log(TAG + " preset '" + presetId + "' registered declaratively")
|
|
1860
1912
|
}
|
|
@@ -1862,7 +1914,7 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1862
1914
|
console.log(TAG + " preset '" + presetId + "' registration failed: " + (error && error.message ? error.message : error))
|
|
1863
1915
|
}
|
|
1864
1916
|
}
|
|
1865
|
-
registeredPython =
|
|
1917
|
+
registeredPython = pythonActive
|
|
1866
1918
|
}).catch((error) => {
|
|
1867
1919
|
console.log(TAG + ' preset reconcile failed: ' + (error && error.message ? error.message : error))
|
|
1868
1920
|
})
|
|
@@ -1870,6 +1922,7 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1870
1922
|
}
|
|
1871
1923
|
|
|
1872
1924
|
await reconcile()
|
|
1925
|
+
if (closed) return
|
|
1873
1926
|
console.log(TAG + ' registered ' + live.size + ' preset(s) declaratively (' + [...live.keys()].join(', ') + ')'
|
|
1874
1927
|
+ (live.size === 0 ? ' — variantPresets is off: the official presets carry Git Bash through the inline tool patch (see the CHANGELOG)'
|
|
1875
1928
|
: '')
|
|
@@ -1932,7 +1985,8 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
|
|
|
1932
1985
|
console.log(TAG + ' dedupe switch wiring failed: ' + (error && error.message ? error.message : error))
|
|
1933
1986
|
}
|
|
1934
1987
|
|
|
1935
|
-
|
|
1988
|
+
// If settings appears after an OFF transition failed closed, retry it.
|
|
1989
|
+
ctx.inject(['settings'], () => { void reconcile() })
|
|
1936
1990
|
}
|
|
1937
1991
|
|
|
1938
1992
|
export async function apply(ctx, config = {}) {
|
|
@@ -2611,8 +2665,8 @@ export async function apply(ctx, config = {}) {
|
|
|
2611
2665
|
// official tool surface mirrored call-for-call) plus a HIDE on the real pwsh
|
|
2612
2666
|
// tool while the pwsh switch is off (route B: the official executor stays in
|
|
2613
2667
|
// place, so pwsh is real PowerShell and hiding it is a product choice). The
|
|
2614
|
-
//
|
|
2615
|
-
//
|
|
2668
|
+
// Full Windows variants keep the same pwsh-only face as official presets,
|
|
2669
|
+
// so they receive this same private-executor patch. Minimal stays persistent.
|
|
2616
2670
|
if (process.platform === 'win32') {
|
|
2617
2671
|
try {
|
|
2618
2672
|
mountInlineTools(ctx, {
|
|
@@ -2661,55 +2715,10 @@ export async function apply(ctx, config = {}) {
|
|
|
2661
2715
|
// With variantPresets ON nothing here runs: the variants stay registered,
|
|
2662
2716
|
// so a variant default keeps working as-is.
|
|
2663
2717
|
try {
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
const settings = settingsCtx.settings
|
|
2669
|
-
if (!settings || typeof settings.update !== 'function') return
|
|
2670
|
-
// The switch is the whole contract: keep-old-modes ON means a
|
|
2671
|
-
// variant default is a legitimate choice and stays untouched.
|
|
2672
|
-
if (liveSettings.variantPresets() === true) return
|
|
2673
|
-
const readDefault = () => { try { return ctx.agentPresets.defaultId } catch { return undefined } }
|
|
2674
|
-
const current = readDefault()
|
|
2675
|
-
if (typeof current !== 'string' || !PRESET_IDS.includes(current)) return
|
|
2676
|
-
const target = VARIANT_DEFAULT_SUCCESSORS[current]
|
|
2677
|
-
if (typeof target !== 'string') return
|
|
2678
|
-
// The successor must exist in the CURRENT roster: official ids
|
|
2679
|
-
// renamed once already (code→ptc), and writing an unknown id
|
|
2680
|
-
// would trade a known failure for a worse one.
|
|
2681
|
-
const roster = await ctx.agentPresets.list()
|
|
2682
|
-
if (!Array.isArray(roster) || !roster.some((row) => row && row.id === target)) {
|
|
2683
|
-
console.warn(TAG + ' default-preset migration stood down: the deployment default is the variant "'
|
|
2684
|
-
+ current + '" but the successor "' + target + '" is not registered on this host.'
|
|
2685
|
-
+ ' Set the default manually among the OFFICIAL presets, or turn variantPresets on.')
|
|
2686
|
-
return
|
|
2687
|
-
}
|
|
2688
|
-
try {
|
|
2689
|
-
await settings.update('agent-preset-registry', { selectedDefault: target })
|
|
2690
|
-
} catch (error) {
|
|
2691
|
-
console.warn(TAG + ' default-preset migration write failed (' + (error?.message ?? error)
|
|
2692
|
-
+ '): the deployment default is still the variant "' + current
|
|
2693
|
-
+ '" — NEW sessions would fail with agent-preset/not-found.'
|
|
2694
|
-
+ ' Fix: set an OFFICIAL default in the preset picker, or turn variantPresets on.')
|
|
2695
|
-
return
|
|
2696
|
-
}
|
|
2697
|
-
const after = readDefault()
|
|
2698
|
-
if (after === target) {
|
|
2699
|
-
console.log(TAG + ' deployment default migrated: "' + current + '" -> "' + target
|
|
2700
|
-
+ '" (variantPresets is off). Old sessions pinned to a variant still need a NEW'
|
|
2701
|
-
+ ' session — the official agent-preset/locked rule blocks a mid-session switch.')
|
|
2702
|
-
} else {
|
|
2703
|
-
console.warn(TAG + ' default-preset migration did not stick (read back "'
|
|
2704
|
-
+ (after === undefined ? 'unreadable' : after) + '"). Set the default manually in'
|
|
2705
|
-
+ ' Settings -> agent preset, or turn variantPresets on.')
|
|
2706
|
-
}
|
|
2707
|
-
} catch (error) {
|
|
2708
|
-
console.log(TAG + ' default-preset migration failed: ' + (error?.message ?? error))
|
|
2709
|
-
}
|
|
2710
|
-
})()
|
|
2711
|
-
})
|
|
2712
|
-
}
|
|
2718
|
+
ctx.inject(['settings'], () => {
|
|
2719
|
+
// Boot migration is detached: registry.list() may await the Loader boot.
|
|
2720
|
+
void migrateVariantDefault(ctx, () => liveSettings.variantPresets())
|
|
2721
|
+
})
|
|
2713
2722
|
} catch (error) {
|
|
2714
2723
|
console.log(TAG + ' default-preset migration wiring failed: ' + (error?.message ?? error))
|
|
2715
2724
|
}
|
|
@@ -2727,8 +2736,21 @@ export async function apply(ctx, config = {}) {
|
|
|
2727
2736
|
enabled: () => liveSettings.fastctxTools(),
|
|
2728
2737
|
createRequireFrom: (base) => createRequire(base ?? import.meta.url),
|
|
2729
2738
|
tools: (c) => { try { return c.get('tools') } catch { return undefined } },
|
|
2730
|
-
msysToWin: (p) =>
|
|
2731
|
-
|
|
2739
|
+
msysToWin: (p, execution) => {
|
|
2740
|
+
const dialect = liveSettings.dialect()
|
|
2741
|
+
if (dialect.posixPaths !== true || !dialectApplies(dialect, execution?.agent)) return p
|
|
2742
|
+
if (!dialect.virtualMounts) {
|
|
2743
|
+
return typeof p === 'string' && (MSYS_DRIVE_ROOT.test(p) || MSYS_BARE_DRIVE.test(p))
|
|
2744
|
+
? translateMsysPath(p, null) : p
|
|
2745
|
+
}
|
|
2746
|
+
return translateMsysPath(p, buildTranslateEnv(readConfiguredBashPath()))
|
|
2747
|
+
},
|
|
2748
|
+
echo: (text, execution) => {
|
|
2749
|
+
const dialect = liveSettings.dialect()
|
|
2750
|
+
if (dialect.posixPaths !== true || !dialectApplies(dialect, execution?.agent)) return text
|
|
2751
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(readConfiguredBashPath()) : null
|
|
2752
|
+
return env ? msysEcho(text, env) : windowsToMsys(text)
|
|
2753
|
+
},
|
|
2732
2754
|
log: (m) => console.log(TAG + ' ' + m),
|
|
2733
2755
|
})
|
|
2734
2756
|
} catch (error) {
|
package/src/inline-tools.js
CHANGED
|
@@ -191,8 +191,25 @@ export async function mountPrivateExecutor(ctx, GitBashSandboxExecutor, createSc
|
|
|
191
191
|
*/
|
|
192
192
|
export function mountInlineTools(ctx, deps) {
|
|
193
193
|
const { tools, agents, createScope, pwshEnabled, log } = deps
|
|
194
|
-
/** agent
|
|
194
|
+
/** Per-agent mounts, masks and cancellation; `agents` above is the host service. */
|
|
195
195
|
const tracked = new Map()
|
|
196
|
+
const pending = new Set()
|
|
197
|
+
const controller = new AbortController()
|
|
198
|
+
let closed = false
|
|
199
|
+
const isLive = (agent, state) => !closed && tracked.get(agent) === state && !state.controller.signal.aborted
|
|
200
|
+
const newState = () => ({ timer: undefined, scope: undefined, denyPwsh: undefined, pending: undefined, controller: new AbortController() })
|
|
201
|
+
// Loader imports cannot be cancelled, but no continuation may outlive its
|
|
202
|
+
// owner. Join our cancelled continuations without waiting on a stuck loader.
|
|
203
|
+
const untilDisposed = async (promise, signal) => {
|
|
204
|
+
signal.throwIfAborted()
|
|
205
|
+
let abort
|
|
206
|
+
const cancelled = new Promise((_, reject) => {
|
|
207
|
+
abort = () => reject(signal.reason)
|
|
208
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
209
|
+
})
|
|
210
|
+
try { return await Promise.race([promise, cancelled]) }
|
|
211
|
+
finally { signal.removeEventListener('abort', abort) }
|
|
212
|
+
}
|
|
196
213
|
/** Resolved once per process: { executor, executorScope, module }. */
|
|
197
214
|
let bashOnce
|
|
198
215
|
/** Set as soon as any face shows ops_* tools (the call-layer guard reads it). */
|
|
@@ -214,58 +231,90 @@ export function mountInlineTools(ctx, deps) {
|
|
|
214
231
|
* declarations — the §4k schemastery semantics come for free), so it never
|
|
215
232
|
* touches the root `ctx.shell` service slot the official pwsh executor
|
|
216
233
|
* owns. The executor scope lives for the plugin's lifetime and is disposed
|
|
217
|
-
* by the effect registered
|
|
234
|
+
* by the ownership effect registered before lifecycle wiring.
|
|
218
235
|
*/
|
|
219
236
|
const bashOncePromise = async () => {
|
|
237
|
+
controller.signal.throwIfAborted()
|
|
220
238
|
if (bashOnce) return bashOnce
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
throw new Error('executor
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
239
|
+
const mounting = (async () => {
|
|
240
|
+
let executorScope
|
|
241
|
+
try {
|
|
242
|
+
const { GitBashSandboxExecutor } = await untilDisposed(import('./shell.js'), controller.signal)
|
|
243
|
+
if (!GitBashSandboxExecutor) throw new Error('executor class unavailable')
|
|
244
|
+
if (!GitBashSandboxExecutor.Config) {
|
|
245
|
+
throw new Error('executor Config unavailable (schemastery unresolved; see the schemastery log lines above)')
|
|
246
|
+
}
|
|
247
|
+
if (typeof createScope !== 'function') throw new Error('no createScope seam')
|
|
248
|
+
controller.signal.throwIfAborted()
|
|
249
|
+
const mounted = await mountPrivateExecutor(ctx, GitBashSandboxExecutor, createScope)
|
|
250
|
+
executorScope = mounted.executorScope
|
|
251
|
+
controller.signal.throwIfAborted()
|
|
252
|
+
const hostModules = {}
|
|
253
|
+
for (const [key, name] of HOST_MODULES) {
|
|
254
|
+
hostModules[key] = await untilDisposed(resolveHostModule(ctx, name), controller.signal)
|
|
255
|
+
}
|
|
256
|
+
const { createBashToolModule } = await untilDisposed(import('./bash-tool.js'), controller.signal)
|
|
257
|
+
const module = createBashToolModule({ executor: mounted.executor, log, ...hostModules })
|
|
258
|
+
return { executor: mounted.executor, executorScope, module }
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (executorScope) await executorScope.dispose()
|
|
261
|
+
throw error
|
|
234
262
|
}
|
|
235
|
-
const { createBashToolModule } = await import('./bash-tool.js')
|
|
236
|
-
const module = createBashToolModule({ executor, log, ...hostModules })
|
|
237
|
-
return { executor, executorScope, module }
|
|
238
263
|
})()
|
|
239
|
-
bashOnce
|
|
240
|
-
|
|
264
|
+
bashOnce = mounting
|
|
265
|
+
mounting.catch(() => { if (bashOnce === mounting) bashOnce = undefined })
|
|
266
|
+
return mounting
|
|
241
267
|
}
|
|
242
268
|
|
|
243
269
|
/** Register the own bash tool inside one agent-owned scope. */
|
|
244
270
|
const provideBash = async (agent, state) => {
|
|
245
|
-
|
|
271
|
+
let scope
|
|
272
|
+
let retained = false
|
|
246
273
|
try {
|
|
247
|
-
const { executor, module } = await bashOncePromise()
|
|
274
|
+
const { executor, module } = await untilDisposed(bashOncePromise(), state.controller.signal)
|
|
275
|
+
if (!isLive(agent, state)) return
|
|
248
276
|
log(`own bash tool ready (private Git Bash executor at ${executor.bashPath || '(unresolved)'}; official pwsh executor untouched)`)
|
|
249
|
-
|
|
277
|
+
scope = createScope(agent.ctx)
|
|
278
|
+
state.scope = scope // Own the scope BEFORE either asynchronous mount.
|
|
250
279
|
// Same asynchronous-fiber shape the official module needed: wait for
|
|
251
280
|
// the apply callback to capture scope.ctx before touching its tools.
|
|
252
|
-
if (scope.fiber)
|
|
281
|
+
if (scope.fiber) await untilDisposed(scope.fiber, state.controller.signal)
|
|
282
|
+
if (!isLive(agent, state)) return
|
|
253
283
|
const registry = scope.ctx?.get('tools')
|
|
254
|
-
if (!registry || typeof scope.ctx.plugin !== 'function')
|
|
255
|
-
void scope.dispose().catch(() => {})
|
|
256
|
-
return
|
|
257
|
-
}
|
|
284
|
+
if (!registry || typeof scope.ctx.plugin !== 'function') return
|
|
258
285
|
const fiber = scope.ctx.plugin(module)
|
|
259
|
-
await fiber
|
|
260
|
-
state
|
|
286
|
+
await untilDisposed(fiber, state.controller.signal)
|
|
287
|
+
if (!isLive(agent, state)) return
|
|
288
|
+
retained = true
|
|
261
289
|
log('own bash tool activated for this agent (Git Bash execution; official pwsh tool real but hidden by default)')
|
|
262
290
|
} catch (error) {
|
|
263
|
-
log(`own bash tool activation failed for this agent: ${error?.message ?? error}`)
|
|
291
|
+
if (isLive(agent, state)) log(`own bash tool activation failed for this agent: ${error?.message ?? error}`)
|
|
292
|
+
} finally {
|
|
293
|
+
if (scope && !retained) {
|
|
294
|
+
if (state.scope === scope) state.scope = undefined
|
|
295
|
+
await scope.dispose()
|
|
296
|
+
}
|
|
264
297
|
}
|
|
265
298
|
}
|
|
266
299
|
|
|
300
|
+
const startBash = (agent, state) => {
|
|
301
|
+
if (state.scope || state.pending || !isLive(agent, state)) return
|
|
302
|
+
const task = provideBash(agent, state)
|
|
303
|
+
state.pending = task
|
|
304
|
+
pending.add(task)
|
|
305
|
+
void task.finally(() => {
|
|
306
|
+
pending.delete(task)
|
|
307
|
+
if (state.pending === task) state.pending = undefined
|
|
308
|
+
}).catch((error) => log(`own bash tool cleanup failed: ${error?.message ?? error}`))
|
|
309
|
+
}
|
|
310
|
+
|
|
267
311
|
const reconcile = (agent, { attempt = 0 } = {}) => {
|
|
268
|
-
if (!agent?.ctx) return
|
|
312
|
+
if (closed || !agent?.ctx) return
|
|
313
|
+
// Undo ONLY our mask before observing the face. schemas() filters a
|
|
314
|
+
// restricted-away pwsh, so it cannot tell us what an ON flip must restore.
|
|
315
|
+
const previous = tracked.get(agent)
|
|
316
|
+
if (previous?.timer) { clearTimeout(previous.timer); previous.timer = undefined }
|
|
317
|
+
if (previous?.denyPwsh) { previous.denyPwsh(); previous.denyPwsh = undefined }
|
|
269
318
|
const names = faceNames(agent)
|
|
270
319
|
// The composed face may not be wired yet at created-time. Retry bounded:
|
|
271
320
|
// a face with NEITHER tool carries no verdict. Five tries at 250ms, then
|
|
@@ -275,7 +324,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
275
324
|
timer.unref?.()
|
|
276
325
|
const state = tracked.get(agent)
|
|
277
326
|
if (state) state.timer = timer
|
|
278
|
-
else tracked.set(agent, {
|
|
327
|
+
else tracked.set(agent, { ...newState(), timer })
|
|
279
328
|
return
|
|
280
329
|
}
|
|
281
330
|
const pwshVisible = deps.pwshEnabled?.() === true
|
|
@@ -284,7 +333,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
284
333
|
let state = tracked.get(agent)
|
|
285
334
|
if (state?.timer) { clearTimeout(state.timer); state.timer = undefined }
|
|
286
335
|
if (!state) {
|
|
287
|
-
state =
|
|
336
|
+
state = newState()
|
|
288
337
|
tracked.set(agent, state)
|
|
289
338
|
}
|
|
290
339
|
// The pwsh face (route B): the host tool is REAL PowerShell now, so the
|
|
@@ -307,22 +356,46 @@ export function mountInlineTools(ctx, deps) {
|
|
|
307
356
|
}
|
|
308
357
|
}
|
|
309
358
|
if (needs.needsBash && !state.scope) {
|
|
310
|
-
|
|
359
|
+
startBash(agent, state)
|
|
311
360
|
}
|
|
312
361
|
}
|
|
313
362
|
|
|
314
|
-
const teardown = (agent) => {
|
|
363
|
+
const teardown = async (agent) => {
|
|
315
364
|
const state = tracked.get(agent)
|
|
316
365
|
if (!state) return
|
|
317
366
|
tracked.delete(agent)
|
|
367
|
+
state.controller.abort(new Error('inline bash agent disposed'))
|
|
318
368
|
if (state.timer) clearTimeout(state.timer)
|
|
319
|
-
if (state.denyPwsh) {
|
|
320
|
-
|
|
369
|
+
if (state.denyPwsh) { state.denyPwsh(); state.denyPwsh = undefined }
|
|
370
|
+
await Promise.all([state.scope?.dispose(), state.pending])
|
|
321
371
|
}
|
|
322
372
|
|
|
373
|
+
// Register ownership before the sweep starts asynchronous work. A cancelled
|
|
374
|
+
// loader continuation and every per-agent mount must settle before disposal.
|
|
375
|
+
ctx.effect(() => async () => {
|
|
376
|
+
closed = true
|
|
377
|
+
const mounting = bashOnce
|
|
378
|
+
controller.abort(new Error('inline bash plugin disposed'))
|
|
379
|
+
await Promise.all([
|
|
380
|
+
...[...tracked.keys()].map(teardown),
|
|
381
|
+
...pending,
|
|
382
|
+
mounting?.then(({ executorScope }) => executorScope.dispose(), () => {}),
|
|
383
|
+
])
|
|
384
|
+
tracked.clear()
|
|
385
|
+
bashOnce = undefined
|
|
386
|
+
}, 'dsh-gitbash-shell: inline tool patching')
|
|
387
|
+
|
|
323
388
|
try {
|
|
324
389
|
ctx.on('agent/created', ({ agent } = {}) => { try { reconcile(agent) } catch { /* never break agent creation */ } })
|
|
325
|
-
ctx.on('agent/disposed', ({ agent } = {}) =>
|
|
390
|
+
ctx.on('agent/disposed', ({ agent } = {}) => teardown(agent).catch((error) => {
|
|
391
|
+
log(`agent bash tool cleanup failed: ${error?.message ?? error}`)
|
|
392
|
+
}))
|
|
393
|
+
ctx.on('loader/volatile-update', (paths) => {
|
|
394
|
+
if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && path[0] === 'pwshTool')) return
|
|
395
|
+
for (const agent of tracked.keys()) {
|
|
396
|
+
try { reconcile(agent) } catch (error) { log(`pwsh switch update failed: ${error?.message ?? error}`) }
|
|
397
|
+
}
|
|
398
|
+
})
|
|
326
399
|
// MOUNT-TIME SWEEP (v0.35.2): agents that already exist when this row
|
|
327
400
|
// mounts never fire agent/created. Same shape the proven dsh-ops denial
|
|
328
401
|
// uses.
|
|
@@ -346,16 +419,6 @@ export function mountInlineTools(ctx, deps) {
|
|
|
346
419
|
return
|
|
347
420
|
}
|
|
348
421
|
|
|
349
|
-
ctx.effect(() => () => {
|
|
350
|
-
for (const agent of [...tracked.keys()]) teardown(agent)
|
|
351
|
-
tracked.clear()
|
|
352
|
-
if (bashOnce) {
|
|
353
|
-
void Promise.resolve(bashOnce).then(({ executorScope } = {}) => {
|
|
354
|
-
try { executorScope?.dispose?.() } catch { /* already gone */ }
|
|
355
|
-
}).catch(() => {})
|
|
356
|
-
bashOnce = undefined
|
|
357
|
-
}
|
|
358
|
-
}, 'dsh-gitbash-shell: inline tool patching')
|
|
359
422
|
}
|
|
360
423
|
|
|
361
424
|
// The host-package resolution channel lives in bash-tool.js; import it for
|