dsh-gitbash-shell 0.24.2 → 0.24.4

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.24.2",
3
+ "version": "0.24.4",
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.24.2",
3
+ "version": "0.24.4",
4
4
  "description": "Windows 上让 agent 的每条 shell 命令改经 Git for Windows bash 执行;物化四个内置预设的 Git Bash 变体,统一 MSYS 盘根路径方言,侧栏终端接管可开关。· Run every agent shell command through Git for Windows bash on Windows; materialize Git Bash variants of the built-in presets, unify the MSYS drive-root path dialect, and adopt the sidebar terminal behind a switch.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -12,7 +12,8 @@
12
12
  ".": "./src/index.js",
13
13
  "./shell": "./src/shell.js",
14
14
  "./client": "./src/client.js",
15
- "./locale/*.json": "./locale/*.json"
15
+ "./locale/*.json": "./locale/*.json",
16
+ "./package.json": "./package.json"
16
17
  },
17
18
  "engines": {
18
19
  "node": ">=18",
@@ -58,8 +59,14 @@
58
59
  "license": "MIT",
59
60
  "peerDependencies": {
60
61
  "@deepseek-ai/cordis": "^4.0.1",
62
+ "@deepseek-ai/dsh": ">=0.1.0",
61
63
  "@deepseek-ai/schemastery": "^3.18.1"
62
64
  },
65
+ "peerDependenciesMeta": {
66
+ "@deepseek-ai/dsh": {
67
+ "optional": true
68
+ }
69
+ },
63
70
  "publishConfig": {
64
71
  "access": "public"
65
72
  },
@@ -261,7 +261,7 @@ export function minimalPluginsFor() {
261
261
  "* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.",
262
262
  "* Please avoid commands that may produce a very large amount of output.",
263
263
  "* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.",
264
- ].join('\n') + '\n',
264
+ ].join('\n'),
265
265
  },
266
266
  },
267
267
  ],
package/src/index.js CHANGED
@@ -52,11 +52,36 @@ import {
52
52
  } from 'node:fs'
53
53
  import { homedir, tmpdir } from 'node:os'
54
54
  import { dirname, join } from 'node:path'
55
- import Schema from '@deepseek-ai/schemastery'
56
55
  import { fileURLToPath } from 'node:url'
57
56
 
58
57
  import { PRESET_META, minimalPluginsFor, pluginsFor } from './compositions.js'
59
58
 
59
+ /**
60
+ * The schemastery module, resolved LAZILY (v0.24.3): `@deepseek-ai/schemastery`
61
+ * is a PEER — plain Node cannot resolve it from this package, only the host's
62
+ * own resolution (the profile shared fallback) supplies it. A deployment whose
63
+ * fallback lacks it used to fail the *static* import at the top of this file,
64
+ * and the Loader treats a failed plugin import as a non-fatal skip
65
+ * (`vendor/loader/src/config/entry.ts` `_init()`: logger.error + return, no
66
+ * fiber) — the row then silently never mounted, which for THIS plugin means no
67
+ * preset variants, no executor wiring and no client half, while every host log
68
+ * stays green. Same all-green failure class as dsh-better-workspace issue #9;
69
+ * verified with a fixture plugin whose only difference was the static peer
70
+ * import. Deferring the import hides the schema where the module is absent
71
+ * instead of killing the row.
72
+ */
73
+ let Schema = null
74
+ try {
75
+ Schema = (await import('@deepseek-ai/schemastery')).default
76
+ } catch (error) {
77
+ Schema = null
78
+ console.warn(
79
+ '[gitbash-shell] @deepseek-ai/schemastery is not resolvable here; the row Config surface is absent'
80
+ + ' (the preset registration and the client half do not depend on it): '
81
+ + (error && error.message || String(error)),
82
+ )
83
+ }
84
+
60
85
  /** Plugin identity for cordis.yml rows. */
61
86
  export const name = 'dsh-gitbash-shell'
62
87
 
@@ -679,9 +704,11 @@ function live(schema) {
679
704
  * Row Config = the settings surface on dsh >= 0.1.7 (values persist under the
680
705
  * row id; the patch row id is 'gitbash-shell', the same string as the old
681
706
  * settings namespace, so the one-shot legacy settings.yaml import maps old
682
- * user values onto the new home). Inert metadata on older hosts.
707
+ * user values onto the new home). Inert metadata on older hosts, and absent
708
+ * (undefined) when schemastery is unresolvable: cordis then passes the row
709
+ * config through unvalidated instead of the whole row disappearing.
683
710
  */
684
- export const Config = Schema.object({
711
+ export const Config = Schema === null ? undefined : Schema.object({
685
712
  // Row-level knob predating the Config surface (invariant 6): which preset
686
713
  // variants to serve. Ordinary config — an edit remounts the plugin, which
687
714
  // is the right weight for a list that changes the roster.
@@ -1665,10 +1692,22 @@ function adoptSidebarShell(ctx, bashPath, readAdopt) {
1665
1692
 
1666
1693
  const readShell = () => {
1667
1694
  const settings = ctx.get('settings')
1668
- if (!settings || typeof settings.get !== 'function' || typeof settings.update !== 'function') return null
1695
+ if (!settings || typeof settings.update !== 'function') return null
1669
1696
  try {
1670
- const value = settings.get(SIDEBAR_NS)
1671
- return value && typeof value === 'object' && typeof value.terminalShell === 'string' ? value.terminalShell : ''
1697
+ // <=0.1.6: the sidebar registers a settings namespace and `get(ns)` reads
1698
+ // it. >=0.1.7: that API is gone (the service exposes describe/update
1699
+ // only), so the value comes from the row Config's projected form —
1700
+ // without this branch the adoption was silently skipped on the new host.
1701
+ if (typeof settings.get === 'function') {
1702
+ const value = settings.get(SIDEBAR_NS)
1703
+ return value && typeof value === 'object' && typeof value.terminalShell === 'string' ? value.terminalShell : ''
1704
+ }
1705
+ if (typeof settings.describe === 'function') {
1706
+ const entry = settings.describe().find((row) => row !== null && typeof row === 'object' && row.ns === SIDEBAR_NS)
1707
+ const value = entry === undefined ? undefined : entry.value
1708
+ return value && typeof value === 'object' && typeof value.terminalShell === 'string' ? value.terminalShell : ''
1709
+ }
1710
+ return null
1672
1711
  } catch {
1673
1712
  return null // namespace not registered yet / sidebar absent
1674
1713
  }
@@ -2348,4 +2387,4 @@ export async function apply(ctx, config = {}) {
2348
2387
  }
2349
2388
 
2350
2389
  // Test surface: pure helpers, no Cordis context required.
2351
- export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, injectPluginManagerRow, hostHasPluginManagerTools, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }
2390
+ export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, injectPluginManagerRow, hostHasPluginManagerTools, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }
package/src/shell.js CHANGED
@@ -46,16 +46,61 @@ export const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
46
46
  /** Log prefix, matching src/index.js. */
47
47
  const TAG = '[gitbash-shell]'
48
48
 
49
+ /**
50
+ * The plugin row's id, which doubles as the settings namespace on <= 0.1.6 and
51
+ * as the row-config namespace on 0.1.7+ (same string by design).
52
+ */
53
+ const SETTINGS_NAMESPACE = 'gitbash-shell'
54
+
55
+ /**
56
+ * Mark one schema field live-editable when the HOST's schemastery supports it.
57
+ *
58
+ * dsh 0.1.7-alpha.1 (`feat(settings): project volatile Config through
59
+ * profile-backed forms`, #4587) made `LocalBashExecutor.Config` declare
60
+ * `cwd`/`timeoutMs`/`maxTimeoutMs`/`maxOutputBytes`/`maxSpillBytes`/`graceMs`
61
+ * with `.volatile()` and read every one of them through `.get()`
62
+ * (`packages/shell/bash-local/src/index.ts:100-107` and its
63
+ * `assertServiceableBashConfig`/`resolve`). A subclass that REDECLARES those
64
+ * fields as plain values therefore throws
65
+ * `TypeError: config.timeoutMs.get is not a function` on the first shell call
66
+ * and takes the whole `ctx.shell` down with it (issue #6, 0.24.2).
67
+ * `SandboxBashExecutor` — our parent — declares no Config of its own: it
68
+ * inherits `LocalBashExecutor`'s verbatim, so this redeclaration IS the
69
+ * executor's contract.
70
+ *
71
+ * The probe keeps one build serving both eras: hosts whose schemastery has
72
+ * `volatile()` (0.1.7+) get the Volatile refs their base class calls `.get()`
73
+ * on, while older hosts — whose base class reads plain values — keep plain
74
+ * ones. Published schemastery 3.18.2 has no `volatile()`, which is exactly why
75
+ * the probe (not an unconditional call) is required.
76
+ */
77
+ function live(schema) {
78
+ return typeof schema.volatile === 'function' ? schema.volatile() : schema
79
+ }
80
+
81
+ /**
82
+ * Build the executor's Config against a given schemastery module. Exported so
83
+ * the smoke test can drive BOTH eras with a recording stand-in instead of
84
+ * depending on which schemastery happens to be installed here.
85
+ * @param z schemastery module (real one, or the test's recorder).
86
+ * @returns the object schema for the `gitbash-executor` row.
87
+ */
88
+ export function gitBashShellConfig(z) {
89
+ return z.object({
90
+ cwd: live(z.string()),
91
+ timeoutMs: live(z.number().default(120000)),
92
+ maxTimeoutMs: live(z.number().default(600000)),
93
+ maxOutputBytes: live(z.number().default(64000)),
94
+ maxSpillBytes: live(z.number().default(64 * 1024 * 1024)),
95
+ graceMs: live(z.number().default(3000)),
96
+ // Our own knob: the base class neither declares nor `.get()`s it, and
97
+ // `get bashPath()` below reads it as a plain value, so it stays plain.
98
+ bashPath: z.string().default(DEFAULT_GIT_BASH),
99
+ })
100
+ }
101
+
49
102
  /** Resolved configuration: the local executor's knobs, plus the Git Bash path. */
50
- export const Config = z.object({
51
- cwd: z.string(),
52
- timeoutMs: z.number().default(120000),
53
- maxTimeoutMs: z.number().default(600000),
54
- maxOutputBytes: z.number().default(64000),
55
- maxSpillBytes: z.number().default(64 * 1024 * 1024),
56
- graceMs: z.number().default(3000),
57
- bashPath: z.string().default(DEFAULT_GIT_BASH),
58
- })
103
+ export const Config = gitBashShellConfig(z)
59
104
 
60
105
  /** Git Bash executor — mirrors the shipped bash/pwsh sandbox executors. */
61
106
  export class GitBashSandboxExecutor extends SandboxBashExecutor {
@@ -119,15 +164,72 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
119
164
  }
120
165
 
121
166
  /**
122
- * Full-access path with Git Bash argv: the parent's full-access branch
123
- * delegates to LocalBashExecutor.run, which hardcodes the bare `bash`
124
- * name, so override that branch here and keep everything else inherited.
167
+ * Decide whether one spec takes the Git Bash argv path, and how to label it
168
+ * (pure). Full access always does — the shipped executor hardcodes the bare
169
+ * `bash` name there. A Windows confined call does too (issue #1: MSYS2 cannot
170
+ * start under the restricted-token runner). Everything else inherits the
171
+ * parent's sandbox path, which already runs Git Bash because `confine` below
172
+ * substitutes the argv.
173
+ * @param spec the shell request.
174
+ * @returns `{ mode, unconfined }`, or undefined to inherit the parent path.
125
175
  */
176
+ gitBashRoute(spec) {
177
+ const policy = spec.sandboxPolicy
178
+ const mode = policy === undefined ? undefined : policy.mode
179
+ if (mode === undefined) return undefined
180
+ if (mode === 'danger-full-access') return { mode, unconfined: false }
181
+ if (process.platform === 'win32') return { mode, unconfined: true }
182
+ return undefined
183
+ }
184
+
126
185
  /**
127
- * Confined Windows calls cannot use the restricted-token runner (issue
128
- * #1, see the file header): route them through the unconfined argv path
129
- * and label the result honestly so callers can tell.
186
+ * Label one execution handle's foreground result without changing its
187
+ * identity (the parent's `decorateResult` is private to the shipped class, so
188
+ * the same memoized in-place wrapper lives here).
189
+ * @param ex the handle returned by `executeArgv`.
190
+ * @param sandbox sandbox facts to stamp onto the result.
191
+ * @returns the same handle, with `result()` mapped.
130
192
  */
193
+ static decorateExecution(ex, sandbox) {
194
+ const base = ex.result.bind(ex)
195
+ let decorated
196
+ ex.result = () => {
197
+ decorated ??= base().then((result) => ({ ...result, sandbox }))
198
+ return decorated
199
+ }
200
+ return ex
201
+ }
202
+
203
+ /**
204
+ * dsh 0.1.7 execution entry point (the shipped `ShellExecutor` now declares
205
+ * `execute(spec): Promise<ShellExecution>`; `run`/`start` were removed and
206
+ * background work is an `onExpiry: 'none'` execution the caller stops
207
+ * waiting on). Overriding it is what keeps the two Git Bash substitutions
208
+ * alive on the new host: without it the full-access branch spawns the bare
209
+ * `bash` name hardcoded in `LocalBashExecutor.execute`, and a Windows
210
+ * confined call goes back through the restricted-token runner MSYS2 cannot
211
+ * survive.
212
+ * On hosts without `execute` (<= 0.1.6) this method is never called by the
213
+ * runtime; it delegates to `run` so the class still behaves if it is.
214
+ * @param spec resolved execution settings and caller-owned command metadata.
215
+ * @returns the live execution handle.
216
+ */
217
+ execute(spec) {
218
+ if (typeof super.execute !== 'function') return this.run(spec)
219
+ const routed = this.withParityEnv(spec)
220
+ const route = this.gitBashRoute(routed)
221
+ if (route === undefined) return super.execute(routed)
222
+ const argv = [this.bashPath, '-c', routed.command]
223
+ if (route.unconfined) this.warnConfinedUnconfined(route.mode)
224
+ const sandbox = route.unconfined
225
+ ? { mode: route.mode, denied: false, enforcement: 'unconfined' }
226
+ : { mode: route.mode, denied: false }
227
+ // `executeArgv` is the shipped helper for exactly this: run an explicit
228
+ // argv with the executor's own lifecycle, output and deadline semantics.
229
+ return Promise.resolve(this.executeArgv(routed, argv))
230
+ .then((ex) => GitBashSandboxExecutor.decorateExecution(ex, sandbox))
231
+ }
232
+
131
233
  /**
132
234
  * Linux-parity environment for the MODEL's shell (v0.21.1). The official
133
235
  * shell-env registry accepts DSH_* facts only — registering anything else
@@ -146,10 +248,33 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
146
248
  * @param {object} spec the shell request
147
249
  * @returns {object} the request with the parity facts merged in
148
250
  */
149
- withParityEnv(spec) {
251
+ /**
252
+ * The plugin row's live dialect settings, across eras. dsh 0.1.7 replaced the
253
+ * settings namespace API: the service still exists but exposes only
254
+ * `describe()` / `update()` — `get(ns)` is gone — so the old read silently
255
+ * returned undefined on the new host and the Linux-parity env below never
256
+ * applied. The new era reads the row Config's projected values from the form
257
+ * descriptors (the same read dsh-agent-lang uses for the `locale` namespace).
258
+ * @returns the row's settings object, or undefined when unavailable.
259
+ */
260
+ dialectSettings() {
150
261
  try {
151
262
  const settings = this.ctx && typeof this.ctx.get === 'function' ? this.ctx.get('settings') : undefined
152
- const value = settings && typeof settings.get === 'function' ? settings.get('gitbash-shell') : undefined
263
+ if (settings === undefined || settings === null) return undefined
264
+ if (typeof settings.get === 'function') return settings.get(SETTINGS_NAMESPACE)
265
+ if (typeof settings.describe === 'function') {
266
+ const entry = settings.describe().find((row) => row !== null && typeof row === 'object' && row.ns === SETTINGS_NAMESPACE)
267
+ return entry === undefined ? undefined : entry.value
268
+ }
269
+ return undefined
270
+ } catch {
271
+ return undefined
272
+ }
273
+ }
274
+
275
+ withParityEnv(spec) {
276
+ try {
277
+ const value = this.dialectSettings()
153
278
  if (!value || value.posixPaths !== true || value.gitAutocrlf === false) return spec
154
279
  const dshEnv = {
155
280
  ...(spec.dshEnv === undefined ? {} : spec.dshEnv),
@@ -165,6 +290,13 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
165
290
  }
166
291
  }
167
292
 
293
+ /**
294
+ * 0.1.6-era full-access path with Git Bash argv: the parent's full-access
295
+ * branch delegates to LocalBashExecutor.run, which hardcodes the bare `bash`
296
+ * name, so override that branch here and keep everything else inherited.
297
+ * On 0.1.7+ the runtime calls `execute` above instead; this stays as the
298
+ * old-era entry point.
299
+ */
168
300
  async run(spec) {
169
301
  spec = this.withParityEnv(spec)
170
302
  const policy = spec.sandboxPolicy