dsh-gitbash-shell 0.13.2 → 0.14.0
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 +15 -0
- package/README_EN.md +19 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/index.js +205 -6
- package/src/shell.js +57 -15
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://awesome-dsh-plugin.com)
|
|
4
4
|
|
|
5
|
+
简体中文 | [English](README_EN.md)
|
|
6
|
+
|
|
5
7
|
> 让 DeepSeek Harness (dsh) 在 Windows 上**全部使用 Git Bash** 的官方风格插件
|
|
6
8
|
> —— 以 Git for Windows 的 `bash.exe` 替换 PowerShell 执行器,并为所有模式
|
|
7
9
|
> 物化对应的 Git Bash 版 agent preset。
|
|
@@ -37,6 +39,19 @@ era。**preset id 保持 `code-gitbash` 不变**(会话钉在 id 上,改名会
|
|
|
37
39
|
会话报 preset not found)。无论先升级插件还是先升级 dsh,都会自动收敛;用户改
|
|
38
40
|
过的目录照旧不碰。
|
|
39
41
|
|
|
42
|
+
### dsh 0.1.6 适配(工作流引擎行改名)
|
|
43
|
+
|
|
44
|
+
dsh 0.1.6-alpha.1 把内置预设的工作流引擎行 `workflow-worker-thread` 改名为
|
|
45
|
+
`workflow-ptc`,并**删除**了旧包。组合里一行 import 失败会拒绝**整棵 preset 挂载**,
|
|
46
|
+
所以把旧名钉死在资产里的 preset 在新版上会直接不可用。本插件两个拼法都不钉:物化时
|
|
47
|
+
**从宿主自己的内置 preset 现场抄**那一行的 id、包名与 `disabled` 状态
|
|
48
|
+
(`rowFormsOf` / `alignEngineRow`),并让 `tool-ralph` 跟随新版默认的 `disabled: true`。
|
|
49
|
+
改写是纯字符串手术(不解析 YAML,`!!js` 安全)且幂等;探测失败(旧宿主、无 roster)时
|
|
50
|
+
资产保持逐字节原样——**一份资产通吃两个 era,升级顺序无关**。
|
|
51
|
+
|
|
52
|
+
同一版还把 `LocalBashExecutor` 的受保护钩子改成了异步:插件的 `runArgv` 结果解包、
|
|
53
|
+
`start` 的返回形态与 `confine` 的取消信号都按**加载期探测**自适应,新旧宿主行为一致。
|
|
54
|
+
|
|
40
55
|
## 安装(公开 npm 插件,推荐)
|
|
41
56
|
|
|
42
57
|
npm: [dsh-gitbash-shell](https://www.npmjs.com/package/dsh-gitbash-shell),
|
package/README_EN.md
CHANGED
|
@@ -2,10 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://awesome-dsh-plugin.com)
|
|
4
4
|
|
|
5
|
+
[简体中文](README.md) | English
|
|
6
|
+
|
|
5
7
|
> Run **every agent shell command through Git Bash** on Windows with DeepSeek
|
|
6
8
|
> Harness (dsh) — replaces the PowerShell executor and materializes Git Bash
|
|
7
9
|
> variants of all four agent presets.
|
|
8
10
|
|
|
11
|
+
### dsh 0.1.6: the workflow-engine row was renamed
|
|
12
|
+
|
|
13
|
+
dsh 0.1.6-alpha.1 renamed the built-in presets' workflow-engine row from
|
|
14
|
+
`workflow-worker-thread` to `workflow-ptc` and **deleted** the old package. One row
|
|
15
|
+
that fails to import rejects the **whole preset mount**, so a composition pinning the
|
|
16
|
+
old name simply stops working on the new host. This plugin pins neither spelling: at
|
|
17
|
+
materialization it copies that row — id, package and `disabled` state — straight out
|
|
18
|
+
of the host's own built-in preset (`rowFormsOf` / `alignEngineRow`), and aligns
|
|
19
|
+
`tool-ralph` with the new `disabled: true` default. The rewrite is plain string
|
|
20
|
+
surgery (no YAML round-trip, so `!!js` stays safe) and idempotent; a failed probe (old
|
|
21
|
+
host, no roster) leaves the assets byte-for-byte untouched — **one set of assets serves
|
|
22
|
+
both eras, in either upgrade order**.
|
|
23
|
+
|
|
24
|
+
The same release made `LocalBashExecutor`'s protected hooks asynchronous: the
|
|
25
|
+
`runArgv` result unwrapping, the `start` return shape and the `confine` cancellation
|
|
26
|
+
signal are all probed at load time, so both host generations behave identically.
|
|
27
|
+
|
|
9
28
|
## Install (public npm package)
|
|
10
29
|
|
|
11
30
|
```sh
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:用 Git for Windows bash 替换 PowerShell 执行器,并物化 standard/minimal/code/cordis 的 Git Bash 变体 preset。双 era 适配 dsh <= 0.1.1(内置 id 'code')与 >= 0.1.2(改名 'ptc'),启动探测自动选择,preset id 永不变。联动:dsh-ptc-cordis-preset(两者同装时 PTC 创造模式自动物化为 Git Bash 版)+ dsh-better-sidebar(终端 UI 标签与模型 terminal_* 工具自动切到 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "DSH plugin: run every agent shell command through Git for Windows bash on Windows instead of PowerShell. Replaces the pwsh executor with a Git Bash ctx.shell provider and materializes Git Bash variants of the standard/minimal/code/cordis agent presets into your user preset root at startup. On Windows it also adopts dsh-better-sidebar's terminal shell (UI tabs and the model-facing terminal_* tools) through the sidebar's official settings seam, and pairs with dsh-ptc-cordis-preset so the PTC Creation-mode preset materializes as its Git Bash variant. Ships both composition eras for dsh <= 0.1.1 (built-in preset id 'code') and dsh >= 0.1.2 (renamed id 'ptc') — the era is probed per boot and re-materialized automatically after a dsh upgrade, in either upgrade order; preset ids never change so pinned sessions stay valid.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/index.js
CHANGED
|
@@ -231,6 +231,183 @@ async function detectPresentSupport(agentPresets) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/** Row ids the shipped workflow-engine row has answered to (dsh 0.1.6 renamed it). */
|
|
235
|
+
const ENGINE_ROW_IDS = ['workflow-worker-thread', 'workflow-ptc']
|
|
236
|
+
|
|
237
|
+
/** Which built-in preset each of this plugin's variants mirrors, for row-form lookup. */
|
|
238
|
+
export const ROW_SOURCE = {
|
|
239
|
+
'standard-gitbash': 'standard',
|
|
240
|
+
'cordis-gitbash': 'cordis',
|
|
241
|
+
'code-gitbash': 'ptc',
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Leading whitespace of one line (pure). */
|
|
245
|
+
function indentOf(line) {
|
|
246
|
+
return line.slice(0, line.length - line.trimStart().length)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The unquoted value of a name line, or undefined when the line is not one (pure). */
|
|
250
|
+
function quotedName(line) {
|
|
251
|
+
const trimmed = line.trim()
|
|
252
|
+
if (!trimmed.startsWith('name: ')) return undefined
|
|
253
|
+
const raw = trimmed.slice(6).trim()
|
|
254
|
+
if (raw.length > 1 && raw.charAt(0) === "'" && raw.charAt(raw.length - 1) === "'") return raw.slice(1, -1)
|
|
255
|
+
return raw
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Read ONE row's spelling out of a composition text (pure): the name value
|
|
260
|
+
* that follows its id line and whether the row's own block turns it off before
|
|
261
|
+
* the next row starts. Returns undefined when the row is absent, and never
|
|
262
|
+
* throws, so a composition that legitimately lacks the row (minimal) is simply
|
|
263
|
+
* left alone.
|
|
264
|
+
* @param text - composition text.
|
|
265
|
+
* @param rowId - exact id value to locate.
|
|
266
|
+
* @returns the row's spelling, or undefined.
|
|
267
|
+
*/
|
|
268
|
+
export function rowFormOf(text, rowId) {
|
|
269
|
+
const lines = text.split('\n')
|
|
270
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
271
|
+
if (lines[i].trim() !== '- id: ' + rowId) continue
|
|
272
|
+
const nameLine = i + 1 < lines.length ? lines[i + 1] : ''
|
|
273
|
+
const name = quotedName(nameLine)
|
|
274
|
+
if (name === undefined) continue
|
|
275
|
+
let disabled = false
|
|
276
|
+
let disabledIndent = indentOf(nameLine)
|
|
277
|
+
for (let j = i + 2; j < lines.length; j += 1) {
|
|
278
|
+
if (lines[j].trim().startsWith('- id: ')) break
|
|
279
|
+
if (lines[j].trim() === 'disabled: true') {
|
|
280
|
+
disabled = true
|
|
281
|
+
disabledIndent = indentOf(lines[j])
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { id: rowId, name, indent: indentOf(lines[i]), nameIndent: indentOf(nameLine), disabled, disabledIndent }
|
|
286
|
+
}
|
|
287
|
+
return undefined
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Read the two rows this plugin's committed compositions must track out of a
|
|
292
|
+
* SHIPPED composition text (pure). The built-in preset is the authority: it
|
|
293
|
+
* mounts on this host by construction, so whatever it spells is mountable
|
|
294
|
+
* here, on every era, without shipping one asset variant per rename.
|
|
295
|
+
* @param text - a shipped agent.cordis.yml.
|
|
296
|
+
* @returns an object with the engine row and the tool-ralph row; either may be undefined.
|
|
297
|
+
*/
|
|
298
|
+
export function rowFormsOf(text) {
|
|
299
|
+
let engine
|
|
300
|
+
for (const id of ENGINE_ROW_IDS) {
|
|
301
|
+
engine = rowFormOf(text, id)
|
|
302
|
+
if (engine !== undefined) break
|
|
303
|
+
}
|
|
304
|
+
return { engine, ralph: rowFormOf(text, 'tool-ralph') }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Align the workflow-engine row with the host's own spelling (pure,
|
|
309
|
+
* idempotent, plain string surgery — never a YAML round-trip, so !!js literals
|
|
310
|
+
* survive).
|
|
311
|
+
*
|
|
312
|
+
* dsh 0.1.6-alpha.1 renamed this row workflow-worker-thread to workflow-ptc
|
|
313
|
+
* AND deleted the old package. A composition row whose module fails to import
|
|
314
|
+
* rejects the WHOLE preset mount (agent-presets mount.ts), so a committed old
|
|
315
|
+
* spelling breaks every Git Bash preset on the new host while the new spelling
|
|
316
|
+
* breaks them on hosts that still ship only the old package. The materializer
|
|
317
|
+
* therefore copies the row out of the host's own built-in preset instead of
|
|
318
|
+
* pinning either spelling.
|
|
319
|
+
* @param text - composition text.
|
|
320
|
+
* @param form - the host's engine row from rowFormsOf, or undefined to leave the text alone.
|
|
321
|
+
* @param options - engineEnabled forces the row on regardless of the host's own state.
|
|
322
|
+
* @returns the aligned text.
|
|
323
|
+
*/
|
|
324
|
+
export function alignEngineRow(text, form, options) {
|
|
325
|
+
if (!form) return text
|
|
326
|
+
const engineEnabled = Boolean(options && options.engineEnabled)
|
|
327
|
+
let current
|
|
328
|
+
for (const id of ENGINE_ROW_IDS) {
|
|
329
|
+
current = rowFormOf(text, id)
|
|
330
|
+
if (current !== undefined) break
|
|
331
|
+
}
|
|
332
|
+
if (current === undefined) return text
|
|
333
|
+
const head = '- id: ' + current.id + '\n' + current.nameIndent + "name: '" + current.name + "'"
|
|
334
|
+
const wanted = '- id: ' + form.id + '\n' + current.nameIndent + "name: '" + form.name + "'"
|
|
335
|
+
const wantOff = engineEnabled ? false : form.disabled
|
|
336
|
+
let out = text.replace(head, wanted)
|
|
337
|
+
if (wantOff && !current.disabled) {
|
|
338
|
+
out = out.replace(wanted + '\n', wanted + '\n' + current.nameIndent + 'disabled: true\n')
|
|
339
|
+
} else if (!wantOff && current.disabled) {
|
|
340
|
+
out = out.replace(wanted + '\n' + current.disabledIndent + 'disabled: true\n', wanted + '\n')
|
|
341
|
+
}
|
|
342
|
+
return out
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Align the tool-ralph row with the host's own default (pure, idempotent).
|
|
347
|
+
* dsh 0.1.6-alpha.1 ships ralph disabled in every built-in preset: its tool
|
|
348
|
+
* description restricts it to runs the human explicitly asked for. The row
|
|
349
|
+
* still imports on older hosts, so this is a semantics alignment rather than a
|
|
350
|
+
* mount fix — but a composition claiming to mirror the built-in preset should
|
|
351
|
+
* not silently re-enable a tool the deployment turned off.
|
|
352
|
+
* @param text - composition text.
|
|
353
|
+
* @param form - the host's tool-ralph row from rowFormsOf, or undefined.
|
|
354
|
+
* @returns the aligned text.
|
|
355
|
+
*/
|
|
356
|
+
export function alignRalphRow(text, form) {
|
|
357
|
+
if (!form) return text
|
|
358
|
+
const current = rowFormOf(text, 'tool-ralph')
|
|
359
|
+
if (current === undefined) return text
|
|
360
|
+
const head = '- id: tool-ralph\n' + current.nameIndent + "name: '" + current.name + "'"
|
|
361
|
+
if (form.disabled && !current.disabled) {
|
|
362
|
+
return text.replace(head + '\n', head + '\n' + current.nameIndent + 'disabled: true\n')
|
|
363
|
+
}
|
|
364
|
+
if (!form.disabled && current.disabled) {
|
|
365
|
+
return text.replace(head + '\n' + current.disabledIndent + 'disabled: true\n', head + '\n')
|
|
366
|
+
}
|
|
367
|
+
return text
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Probe every built-in preset this plugin's variants mirror for those row
|
|
372
|
+
* spellings (never throws). The live roster is the authority — package
|
|
373
|
+
* resolution alone would lie on linked development trees and on CLI installs
|
|
374
|
+
* whose first-party packages live outside the profile.
|
|
375
|
+
* @param agentPresets - the roster service.
|
|
376
|
+
* @returns a map of row forms per available built-in id, or undefined.
|
|
377
|
+
*/
|
|
378
|
+
async function detectRowForms(agentPresets) {
|
|
379
|
+
try {
|
|
380
|
+
const list = await agentPresets.list()
|
|
381
|
+
const entries = Array.isArray(list) ? list : []
|
|
382
|
+
const out = {}
|
|
383
|
+
for (const id of ['ptc', 'standard', 'cordis']) {
|
|
384
|
+
const entry = entries.find((p) => p && p.id === id && typeof p.path === 'string')
|
|
385
|
+
if (!entry) continue
|
|
386
|
+
const file = /\.yml$/.test(entry.path) ? entry.path : join(entry.path, 'agent.cordis.yml')
|
|
387
|
+
out[id] = rowFormsOf(readFileSync(file, 'utf8'))
|
|
388
|
+
}
|
|
389
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.log(TAG + ' row-form probe failed (' + (error && error.message ? error.message : error) + ') — keeping the committed spellings')
|
|
392
|
+
return undefined
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Marker fingerprint of the row spellings one variant is aligned to, so a host
|
|
398
|
+
* that renames (or re-defaults) them re-materializes on the next boot.
|
|
399
|
+
* @param rows - detectRowForms result.
|
|
400
|
+
* @param presetId - the variant being materialized.
|
|
401
|
+
* @returns a short stable string, '' when nothing was probed.
|
|
402
|
+
*/
|
|
403
|
+
function rowFingerprint(rows, presetId) {
|
|
404
|
+
const source = ROW_SOURCE[presetId]
|
|
405
|
+
const form = rows && source ? rows[source] : undefined
|
|
406
|
+
if (!form || !form.engine) return ''
|
|
407
|
+
const ralph = form.ralph === undefined ? 'na' : (form.ralph.disabled ? 'off' : 'on')
|
|
408
|
+
return form.engine.id + ':' + (form.engine.disabled ? 'off' : 'on') + ':' + ralph
|
|
409
|
+
}
|
|
410
|
+
|
|
234
411
|
/**
|
|
235
412
|
* Pick the committed composition asset inside one variant directory (pure).
|
|
236
413
|
* The ptc-era twin is preferred when the roster says `ptc`; the persona-split
|
|
@@ -250,11 +427,17 @@ function pickComposition(base, persona, available) {
|
|
|
250
427
|
}
|
|
251
428
|
|
|
252
429
|
/** Startup decision for an existing unmodified tree: 'refresh' or 'idle'. */
|
|
253
|
-
function syncDecision({ state, marker, version, sourceHashes, base = 'code', persona = 'text', present = false }) {
|
|
430
|
+
function syncDecision({ state, marker, version, sourceHashes, base = 'code', persona = 'text', present = false, rows = '' }) {
|
|
254
431
|
if (state !== 'unmodified' || !marker) return 'refresh'
|
|
255
432
|
if (marker.version !== version) return 'refresh'
|
|
256
433
|
if (marker.base !== base) return 'refresh'
|
|
257
434
|
if ((marker.persona ?? 'text') !== persona) return 'refresh'
|
|
435
|
+
// Host row-form flip: dsh 0.1.6-alpha.1 renamed the workflow-engine row and
|
|
436
|
+
// deleted the package behind the old spelling. A composition row that cannot
|
|
437
|
+
// be imported rejects the WHOLE preset mount, so a host reporting a
|
|
438
|
+
// different spelling must re-materialize (same marker-dimension pattern as
|
|
439
|
+
// persona and present).
|
|
440
|
+
if ((marker.rows ?? '') !== rows) return 'refresh'
|
|
258
441
|
// Host-capability flip: @deepseek-ai/dsh-tool-present first shipped with dsh
|
|
259
442
|
// 0.1.5-alpha.2. A preset composed with the row on a host that lacks the
|
|
260
443
|
// package REJECTS THE WHOLE MOUNT (mount.ts: a row whose module failed to
|
|
@@ -315,7 +498,7 @@ async function hostHasToolPresent() {
|
|
|
315
498
|
}
|
|
316
499
|
|
|
317
500
|
/** Write one preset directory from scratch. Returns 'ok' or 'no-skills-source'. */
|
|
318
|
-
function materialize({ target, presetId, skillsSource, version, base = 'code', persona = 'text', present = false }) {
|
|
501
|
+
function materialize({ target, presetId, skillsSource, version, base = 'code', persona = 'text', present = false, rows }) {
|
|
319
502
|
rmSync(target, { recursive: true, force: true })
|
|
320
503
|
mkdirSync(target, { recursive: true })
|
|
321
504
|
|
|
@@ -323,6 +506,17 @@ function materialize({ target, presetId, skillsSource, version, base = 'code', p
|
|
|
323
506
|
const compositionFile = pickComposition(base, persona, available)
|
|
324
507
|
let composition = readFileSync(join(pkgDir, 'assets', presetId, compositionFile), 'utf8')
|
|
325
508
|
if (present && compositionFile.includes('.ptc.')) composition = injectPresentRow(composition)
|
|
509
|
+
// Row-form alignment (v0.14.0): the committed assets pin one spelling of the
|
|
510
|
+
// workflow-engine row, but a host renames it out from under them (dsh
|
|
511
|
+
// 0.1.6-alpha.1) and a row whose module fails to import rejects the whole
|
|
512
|
+
// mount. Copy the host's own spelling rather than shipping a third asset
|
|
513
|
+
// variant per era. Absent probe = leave the frozen history untouched.
|
|
514
|
+
const rowSource = ROW_SOURCE[presetId]
|
|
515
|
+
const rowForm = rows && rowSource ? rows[rowSource] : undefined
|
|
516
|
+
if (rowForm) {
|
|
517
|
+
composition = alignEngineRow(composition, rowForm.engine)
|
|
518
|
+
composition = alignRalphRow(composition, rowForm.ralph)
|
|
519
|
+
}
|
|
326
520
|
writeFileSync(join(target, 'agent.cordis.yml'), composition)
|
|
327
521
|
writeFileSync(join(target, 'preset.yml'), readFileSync(join(pkgDir, 'assets', presetId, 'preset.yml')))
|
|
328
522
|
|
|
@@ -337,7 +531,7 @@ function materialize({ target, presetId, skillsSource, version, base = 'code', p
|
|
|
337
531
|
}
|
|
338
532
|
}
|
|
339
533
|
|
|
340
|
-
const marker = { managedBy: MANAGED_BY, version, presetId, base, persona, present, files: hashTree(target) }
|
|
534
|
+
const marker = { managedBy: MANAGED_BY, version, presetId, base, persona, present, rows: rowFingerprint(rows, presetId), files: hashTree(target) }
|
|
341
535
|
writeFileSync(join(target, MARKER_FILE), JSON.stringify(marker, null, 2) + '\n')
|
|
342
536
|
return skills
|
|
343
537
|
}
|
|
@@ -951,6 +1145,10 @@ export async function apply(ctx, config = {}) {
|
|
|
951
1145
|
// resolving from this plugin's own tree (covers hosts where the roster
|
|
952
1146
|
// probe is unavailable).
|
|
953
1147
|
const present = (await detectPresentSupport(ctx.agentPresets)) || (await hostHasToolPresent())
|
|
1148
|
+
// Row spellings of the built-in presets these variants mirror: the
|
|
1149
|
+
// workflow-engine row was renamed and its old package deleted in dsh
|
|
1150
|
+
// 0.1.6-alpha.1, so the materializer copies whatever the host itself ships.
|
|
1151
|
+
const rows = await detectRowForms(ctx.agentPresets)
|
|
954
1152
|
const userRootPath = userRoot.path
|
|
955
1153
|
purgeOrphans(userRootPath, presetIds)
|
|
956
1154
|
|
|
@@ -980,12 +1178,13 @@ export async function apply(ctx, config = {}) {
|
|
|
980
1178
|
|
|
981
1179
|
const sourceHashes = presetId === 'cordis-gitbash' ? skillsHashes(skillsSource) : null
|
|
982
1180
|
const marker = readMarker(target)
|
|
983
|
-
|
|
1181
|
+
const markerRows = rowFingerprint(rows, presetId)
|
|
1182
|
+
if (state === 'unmodified' && syncDecision({ state, marker, version, sourceHashes, base, persona, present, rows: markerRows }) === 'idle') {
|
|
984
1183
|
ctx.logger?.('gitbash-shell')?.debug?.( `preset '${presetId}' up to date (v${version}, ${base}-era${persona === 'split' ? ', persona-split' : ''}) — idle`)
|
|
985
1184
|
continue
|
|
986
1185
|
}
|
|
987
1186
|
|
|
988
|
-
const skills = materialize({ target, presetId, skillsSource, version, base, persona, present })
|
|
1187
|
+
const skills = materialize({ target, presetId, skillsSource, version, base, persona, present, rows })
|
|
989
1188
|
const verb = state === 'absent' ? 'materialized' : 'refreshed'
|
|
990
1189
|
console.log(
|
|
991
1190
|
`${TAG} ${verb} preset '${presetId}' into ${userRootPath} (v${version}, ${base}-era composition${persona === 'split' ? ', persona-split' : ''})` +
|
|
@@ -995,4 +1194,4 @@ export async function apply(ctx, config = {}) {
|
|
|
995
1194
|
}
|
|
996
1195
|
|
|
997
1196
|
// Test surface: pure helpers, no Cordis context required.
|
|
998
|
-
export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, readPosixPaths, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport }
|
|
1197
|
+
export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, readPosixPaths, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }
|
package/src/shell.js
CHANGED
|
@@ -70,12 +70,52 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
70
70
|
/**
|
|
71
71
|
* Wrap one shell command via the ctx.sandbox provider, substituting Git Bash
|
|
72
72
|
* for the shipped bare `bash` argv.
|
|
73
|
+
*
|
|
74
|
+
* Signature spans both dsh eras: 0.1.6-alpha.1 made the provider's confine
|
|
75
|
+
* async and handed it the caller's abort signal (the base class now calls
|
|
76
|
+
* `this.confine(command, policy, signal)` and awaits it). The predecessor
|
|
77
|
+
* was synchronous and took two arguments. Forwarding the third argument is
|
|
78
|
+
* safe on both — the old provider ignores it and its synchronous return is
|
|
79
|
+
* what the old base class still consumes — while dropping it on the new host
|
|
80
|
+
* would silently detach sandbox preparation from cancellation.
|
|
73
81
|
* @param command - shell source for the confined inner `bash -c`.
|
|
74
82
|
* @param policy - resolved confined execution policy.
|
|
75
|
-
* @
|
|
83
|
+
* @param signal - caller deadline/cancellation, forwarded when the host provides one.
|
|
84
|
+
* @returns the provider's exact argv and settlement-classification facts (a promise on dsh >= 0.1.6).
|
|
76
85
|
*/
|
|
77
|
-
confine(command, policy) {
|
|
78
|
-
return this.ctx.sandbox.confine([this.bashPath, '-c', command], policy)
|
|
86
|
+
confine(command, policy, signal) {
|
|
87
|
+
return this.ctx.sandbox.confine([this.bashPath, '-c', command], policy, signal)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether the LOADED base class declares an async `start`, probed once
|
|
92
|
+
* (pure). dsh 0.1.6-alpha.1 turned LocalBashExecutor.start into an async
|
|
93
|
+
* method returning Promise<ShellProcess>; before that it returned the live
|
|
94
|
+
* handle directly. This override has to match whichever contract the host
|
|
95
|
+
* actually carries: handing a plain handle to a host that awaits is fine,
|
|
96
|
+
* but handing a Promise to a synchronous host would give it a thenable
|
|
97
|
+
* instead of a process. Shape is probed, never inferred from a version.
|
|
98
|
+
* @returns true when the base class start is an async function.
|
|
99
|
+
*/
|
|
100
|
+
static baseStartIsAsync() {
|
|
101
|
+
return SandboxBashExecutor.prototype.start.constructor.name === 'AsyncFunction'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Unwrap `runArgv`'s settlement across dsh eras (pure). 0.1.6-alpha.1
|
|
106
|
+
* changed the protected hook's return value from the bare ShellRunResult to
|
|
107
|
+
* `{ result, spawnRequested }`, where spawnRequested is false when
|
|
108
|
+
* preparation was cancelled before any spawn. Spreading the wrapper as if it
|
|
109
|
+
* were the result would hand the caller an object carrying no exitCode and
|
|
110
|
+
* no streams at all, so it is unwrapped by shape — never by version.
|
|
111
|
+
* @param raw - whatever the loaded `runArgv` resolved to.
|
|
112
|
+
* @returns the bare result plus whether argv ever reached the provider.
|
|
113
|
+
*/
|
|
114
|
+
static unwrapRunArgv(raw) {
|
|
115
|
+
if (raw !== null && typeof raw === 'object' && typeof raw.spawnRequested === 'boolean' && raw.result !== undefined) {
|
|
116
|
+
return { result: raw.result, spawnRequested: raw.spawnRequested }
|
|
117
|
+
}
|
|
118
|
+
return { result: raw, spawnRequested: true }
|
|
79
119
|
}
|
|
80
120
|
|
|
81
121
|
/**
|
|
@@ -93,13 +133,17 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
93
133
|
if (policy === undefined) return super.run(spec)
|
|
94
134
|
const { mode } = policy
|
|
95
135
|
if (mode === 'danger-full-access') {
|
|
96
|
-
const result =
|
|
136
|
+
const { result } = GitBashSandboxExecutor.unwrapRunArgv(
|
|
137
|
+
await this.runArgv(spec, [this.bashPath, '-c', spec.command]))
|
|
97
138
|
return { ...result, sandbox: { mode, denied: false } }
|
|
98
139
|
}
|
|
99
140
|
if (process.platform === 'win32') {
|
|
100
141
|
this.warnConfinedUnconfined(mode)
|
|
101
|
-
const result =
|
|
102
|
-
|
|
142
|
+
const { result, spawnRequested } = GitBashSandboxExecutor.unwrapRunArgv(
|
|
143
|
+
await this.runArgv(spec, [this.bashPath, '-c', spec.command]))
|
|
144
|
+
// No spawn means no argv ran, so there is nothing to label as
|
|
145
|
+
// unconfined; the parent reports the same bare sandbox facts there.
|
|
146
|
+
return { ...result, sandbox: spawnRequested ? { mode, denied: false, enforcement: 'unconfined' } : { mode, denied: false } }
|
|
103
147
|
}
|
|
104
148
|
return super.run(spec)
|
|
105
149
|
}
|
|
@@ -108,16 +152,14 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
108
152
|
const policy = spec.sandboxPolicy
|
|
109
153
|
if (policy === undefined) return super.start(spec)
|
|
110
154
|
const { mode } = policy
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return proc
|
|
115
|
-
}
|
|
116
|
-
if (process.platform === 'win32') {
|
|
117
|
-
this.warnConfinedUnconfined(mode)
|
|
155
|
+
const fullAccess = mode === 'danger-full-access'
|
|
156
|
+
if (fullAccess || process.platform === 'win32') {
|
|
157
|
+
if (!fullAccess) this.warnConfinedUnconfined(mode)
|
|
118
158
|
const proc = this.startArgv(spec, [this.bashPath, '-c', spec.command])
|
|
119
|
-
proc.sandbox = { mode, denied: false, enforcement: 'unconfined' }
|
|
120
|
-
|
|
159
|
+
proc.sandbox = fullAccess ? { mode, denied: false } : { mode, denied: false, enforcement: 'unconfined' }
|
|
160
|
+
// Match the loaded base contract: the async one (dsh >= 0.1.6) is
|
|
161
|
+
// awaited by its callers, the synchronous one is consumed directly.
|
|
162
|
+
return GitBashSandboxExecutor.baseStartIsAsync() ? Promise.resolve(proc) : proc
|
|
121
163
|
}
|
|
122
164
|
return super.start(spec)
|
|
123
165
|
}
|