dsh-gitbash-shell 0.27.0 → 0.28.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/CHANGELOG.md +37 -0
- package/README.md +21 -0
- package/cordis.patch.yml +5 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/bash-path.js +371 -0
- package/src/client.js +353 -7
- package/src/index.js +84 -23
- package/src/shell.js +58 -10
package/src/index.js
CHANGED
|
@@ -245,8 +245,15 @@ export function effectivePresetIds(configured, { suppress, peerGitBash } = {}) {
|
|
|
245
245
|
return list.filter((id) => id !== PEER_COVERED_PRESET_ID)
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
248
|
+
/**
|
|
249
|
+
* The ONE bash resolver (v0.28.0, issue #11): the executor (src/shell.js) and
|
|
250
|
+
* this translation layer read the same memoized result, so "dialect translated
|
|
251
|
+
* to Q:/Git but the executor still spawns C:/Program Files/Git" cannot happen.
|
|
252
|
+
*/
|
|
253
|
+
import {
|
|
254
|
+
DEFAULT_GIT_BASH, GIT_BASH_DOWNLOAD_URL, bashResolutionReport,
|
|
255
|
+
effectiveConfiguredBashPath, executorConfiguredBashPath, resolveGitBashCached, settingsBashPath,
|
|
256
|
+
} from './bash-path.js'
|
|
250
257
|
|
|
251
258
|
/** The shipped preset whose skills/ dir seeds cordis-gitbash. */
|
|
252
259
|
const SKILLS_SOURCE_PRESET = 'cordis'
|
|
@@ -1448,23 +1455,12 @@ export function buildTranslateEnv(configuredBashPath) {
|
|
|
1448
1455
|
if (h && existsSync(h)) env.home = h.replace(/\\/g, '/')
|
|
1449
1456
|
} catch { /* keep undefined */ }
|
|
1450
1457
|
try {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
}
|
|
1458
|
-
for (const candidate of candidates) {
|
|
1459
|
-
try {
|
|
1460
|
-
if (!existsSync(candidate)) continue
|
|
1461
|
-
const root = dirname(dirname(candidate))
|
|
1462
|
-
if (existsSync(join(root, 'usr', 'bin'))) {
|
|
1463
|
-
env.gitRoot = root.replace(/\\/g, '/')
|
|
1464
|
-
break
|
|
1465
|
-
}
|
|
1466
|
-
} catch { /* try the next candidate */ }
|
|
1467
|
-
}
|
|
1458
|
+
// v0.28.0 (issue #11): ONE resolver for the executor and this layer. Its
|
|
1459
|
+
// chain is setting > default install paths > PATH > registry PATH and it
|
|
1460
|
+
// refuses WSL/MSYS2/Cygwin outright; `cacheKey` is the settings bashPath,
|
|
1461
|
+
// i.e. the same explicit tier the executor reads.
|
|
1462
|
+
const resolution = resolveGitBashCached({ configured: cacheKey })
|
|
1463
|
+
if (resolution.ok) env.gitRoot = resolution.root.replace(/\\/g, '/')
|
|
1468
1464
|
} catch { /* keep undefined */ }
|
|
1469
1465
|
translateEnvCache.set(cacheKey, env)
|
|
1470
1466
|
return env
|
|
@@ -2351,7 +2347,28 @@ export async function apply(ctx, config = {}) {
|
|
|
2351
2347
|
// plugins (e.g. dsh-ptc-cordis-preset) can adopt it in the presets they
|
|
2352
2348
|
// materialize: service present, 'active: true' on Windows, 'active: false'
|
|
2353
2349
|
// where the bundle is installed but the platform stack stayed native.
|
|
2354
|
-
|
|
2350
|
+
/* The explicit tier of the resolution chain, in its documented priority:
|
|
2351
|
+
the `gitbash-executor` row's own config > the `gitbash-shell` row/settings
|
|
2352
|
+
field > empty (= run the automatic chain). Both this layer and the
|
|
2353
|
+
executor resolve from this same value, so an explicit answer is never
|
|
2354
|
+
substituted and never silently ignored.
|
|
2355
|
+
(issue #11: the row config is where the reporter's workaround landed.) */
|
|
2356
|
+
const configuredBashPath = effectiveConfiguredBashPath(executorConfiguredBashPath(ctx), settingsBashPath(ctx))
|
|
2357
|
+
const bashResolution = resolveGitBashCached({ configured: configuredBashPath })
|
|
2358
|
+
const gitBashCapability = {
|
|
2359
|
+
active: process.platform === 'win32',
|
|
2360
|
+
// Kept for existing consumers (peer plugins read `bashPath`): the resolved
|
|
2361
|
+
// interpreter. When resolution failed we report the explicit value (or the
|
|
2362
|
+
// historical default) so the ENOENT a consumer may hit names a real path —
|
|
2363
|
+
// never a substitute shell.
|
|
2364
|
+
bashPath: bashResolution.ok ? bashResolution.path : (configuredBashPath || DEFAULT_GIT_BASH),
|
|
2365
|
+
// Additive (v0.28.0): the full verdict, for the guided popup and peers.
|
|
2366
|
+
ok: bashResolution.ok,
|
|
2367
|
+
source: bashResolution.source,
|
|
2368
|
+
configured: configuredBashPath,
|
|
2369
|
+
tried: bashResolution.tried,
|
|
2370
|
+
downloadUrl: GIT_BASH_DOWNLOAD_URL,
|
|
2371
|
+
}
|
|
2355
2372
|
const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
|
|
2356
2373
|
ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
|
|
2357
2374
|
|
|
@@ -2571,7 +2588,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2571
2588
|
// untouched and the result metadata is not echoed back in MSYS form.
|
|
2572
2589
|
if (dialect.posixPaths && dialectApplies(dialect, exec && exec.agent)) {
|
|
2573
2590
|
echo = true
|
|
2574
|
-
const env = dialect.virtualMounts ? buildTranslateEnv(
|
|
2591
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(configuredBashPath) : null
|
|
2575
2592
|
echoEnv = env
|
|
2576
2593
|
if (exec && exec.arguments && typeof exec.arguments === 'object') {
|
|
2577
2594
|
const translated = translateDispatch(exec, dialect, env)
|
|
@@ -2626,7 +2643,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2626
2643
|
if (!dialectApplies(dialect, exec && exec.agent)) return next()
|
|
2627
2644
|
if (dialect.posixPaths && dialect.errorDialect && exec && result && typeof result === 'object'
|
|
2628
2645
|
&& result.isError === true && Array.isArray(result.content)) {
|
|
2629
|
-
const env = dialect.virtualMounts ? buildTranslateEnv(
|
|
2646
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(configuredBashPath) : null
|
|
2630
2647
|
if (exec.name === 'run_code') {
|
|
2631
2648
|
// paths only — the /dev/null hint is for file tools
|
|
2632
2649
|
const content = rewriteErrorContent(result.content, { nulHint: false, env })
|
|
@@ -2694,12 +2711,56 @@ export async function apply(ctx, config = {}) {
|
|
|
2694
2711
|
}
|
|
2695
2712
|
}
|
|
2696
2713
|
|
|
2714
|
+
// ── fail-loud bash report + the client-visible status route (v0.28.0) ──
|
|
2715
|
+
// issue #11: the old failure mode was "every command dies with
|
|
2716
|
+
// `spawn C:/Program Files/Git/bin/bash.exe ENOENT` and nothing explains it".
|
|
2717
|
+
// Now the boot log names the value, the whole probe chain and where to fix
|
|
2718
|
+
// it, and the client half reads the same verdict over HTTP to raise the
|
|
2719
|
+
// guided popup. Nothing here substitutes another shell — by design.
|
|
2720
|
+
console.log(TAG + ' bash resolution: ' + bashResolutionReport(bashResolution).split('\n').join('\n' + TAG + ' '))
|
|
2721
|
+
if (process.platform === 'win32') {
|
|
2722
|
+
try {
|
|
2723
|
+
ctx.inject(['webServer'], (wctx) => {
|
|
2724
|
+
try {
|
|
2725
|
+
const route = '/dsh-gitbash-shell/api/status'
|
|
2726
|
+
const read = () => {
|
|
2727
|
+
const resolution = resolveGitBashCached({ configured: configuredBashPath, fresh: true })
|
|
2728
|
+
return {
|
|
2729
|
+
ok: resolution.ok,
|
|
2730
|
+
platform: process.platform,
|
|
2731
|
+
resolved: resolution.path,
|
|
2732
|
+
root: resolution.root,
|
|
2733
|
+
source: resolution.source,
|
|
2734
|
+
configured: resolution.configured,
|
|
2735
|
+
tried: resolution.tried,
|
|
2736
|
+
downloadUrl: GIT_BASH_DOWNLOAD_URL,
|
|
2737
|
+
settingsHint: 'Settings -> Plugins -> dsh-gitbash-shell -> "Git Bash path"',
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const handler = (req, res) => {
|
|
2741
|
+
const text = JSON.stringify(read())
|
|
2742
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'content-length': Buffer.byteLength(text) })
|
|
2743
|
+
res.end(text)
|
|
2744
|
+
}
|
|
2745
|
+
const dispose = wctx.webServer.register({ kind: 'prefix', path: route, handler })
|
|
2746
|
+
wctx.effect(() => dispose, 'dsh-gitbash-shell: bash status route')
|
|
2747
|
+
console.log(TAG + ' bash status route active at ' + route + ' (read by the guided popup)')
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
console.log(TAG + ' bash status route failed: ' + (error?.message ?? error))
|
|
2750
|
+
}
|
|
2751
|
+
})
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
console.log(TAG + ' bash status route wiring failed: ' + (error?.message ?? error))
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2697
2757
|
// ── dsh-better-sidebar terminal adoption (Windows only) ────────────────
|
|
2698
2758
|
// The sidebar resolves its terminal shell through the settings seam per
|
|
2699
2759
|
// open; adopt it through that seam (see adoptSidebarShell for rationale).
|
|
2700
2760
|
// Disable with `betterSidebarShell: false` in the plugin row config.
|
|
2701
2761
|
if (config.betterSidebarShell !== false && process.platform === 'win32') {
|
|
2702
|
-
adoptSidebarShell(ctx,
|
|
2762
|
+
if (bashResolution.ok) adoptSidebarShell(ctx, bashResolution.path, () => liveSettings.adoptSidebar())
|
|
2763
|
+
else console.log(TAG + ' sidebar terminal adoption skipped: no Git Bash resolved (nothing is substituted)')
|
|
2703
2764
|
}
|
|
2704
2765
|
|
|
2705
2766
|
// ── era split: declarative registration on dsh >= 0.1.7 ──────────────────
|
package/src/shell.js
CHANGED
|
@@ -38,10 +38,13 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|
|
41
|
+
|
|
42
|
+
import { DEFAULT_GIT_BASH, bashResolutionReport, effectiveConfiguredBashPath, resolveGitBashCached, settingsBashPath } from './bash-path.js'
|
|
43
|
+
|
|
44
|
+
/** Re-exported for callers that historically read it here (the value now lives in bash-path.js). */
|
|
45
|
+
export { DEFAULT_GIT_BASH }
|
|
41
46
|
import z from '@deepseek-ai/schemastery'
|
|
42
47
|
|
|
43
|
-
/** Default Git for Windows bash (forward slashes work on Windows too). */
|
|
44
|
-
export const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
|
|
45
48
|
|
|
46
49
|
/** Log prefix, matching src/index.js. */
|
|
47
50
|
const TAG = '[gitbash-shell]'
|
|
@@ -95,7 +98,12 @@ export function gitBashShellConfig(z) {
|
|
|
95
98
|
graceMs: live(z.number().default(3000)),
|
|
96
99
|
// Our own knob: the base class neither declares nor `.get()`s it, and
|
|
97
100
|
// `get bashPath()` below reads it as a plain value, so it stays plain.
|
|
98
|
-
|
|
101
|
+
//
|
|
102
|
+
// EMPTY means AUTO (v0.28.0, issue #11): the shared resolver walks
|
|
103
|
+
// setting > default install paths > PATH > registry PATH and refuses WSL /
|
|
104
|
+
// MSYS2 / Cygwin. A hard-coded default here is what made a Git installed
|
|
105
|
+
// outside C:/Program Files look like "dsh is broken".
|
|
106
|
+
bashPath: z.string().default(''),
|
|
99
107
|
})
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -107,9 +115,49 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
107
115
|
static inject = ['subprocess', 'sandbox', 'sandboxPolicy']
|
|
108
116
|
static Config = Config
|
|
109
117
|
|
|
110
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Effective Git Bash path. The configured value wins when it is non-empty
|
|
120
|
+
* (the user's explicit answer is never substituted); otherwise the SHARED
|
|
121
|
+
* resolver decides — the very same result the path-dialect translation layer
|
|
122
|
+
* uses, so "dialect works, executor cannot find bash" (issue #11) is
|
|
123
|
+
* structurally impossible.
|
|
124
|
+
*
|
|
125
|
+
* A failed resolution returns '' — deliberately NOT a substitute shell. Every
|
|
126
|
+
* argv built from it then fails loudly on spawn; `apply()` has already
|
|
127
|
+
* printed the full report, and the client half shows the guided popup.
|
|
128
|
+
*/
|
|
111
129
|
get bashPath() {
|
|
112
|
-
return this.
|
|
130
|
+
return this.bashResolution().path
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve the interpreter with the SAME merged explicit tier the main plugin
|
|
135
|
+
* uses (executor row config > `gitbash-shell` settings row > empty = auto),
|
|
136
|
+
* so both halves of the plugin always agree on which bash this process runs.
|
|
137
|
+
* @returns {object} the memoized resolution verdict.
|
|
138
|
+
*/
|
|
139
|
+
bashResolution() {
|
|
140
|
+
const configured = effectiveConfiguredBashPath(this.config.bashPath, settingsBashPath(this.ctx))
|
|
141
|
+
return resolveGitBashCached({ configured })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The resolved Git Bash path, or a THROW that says exactly what is wrong.
|
|
146
|
+
*
|
|
147
|
+
* The throw is the design (AGENTS.md §4h): with no Git Bash there is no
|
|
148
|
+
* command execution at all — we do not substitute pwsh, cmd, WSL or MSYS2,
|
|
149
|
+
* and we do not let a bare `spawn ENOENT` be the only thing the user sees.
|
|
150
|
+
* The boot log carries the same report (src/index.js) and the client half
|
|
151
|
+
* shows the guided popup with the probed list.
|
|
152
|
+
* @returns {string} the resolved interpreter path.
|
|
153
|
+
*/
|
|
154
|
+
requireBashPath() {
|
|
155
|
+
const resolution = this.bashResolution()
|
|
156
|
+
if (!resolution.ok) {
|
|
157
|
+
throw new Error('dsh-gitbash-shell: no Git for Windows bash found, so this command cannot run. '
|
|
158
|
+
+ bashResolutionReport(resolution).split('\n').slice(1).join(' | '))
|
|
159
|
+
}
|
|
160
|
+
return resolution.path
|
|
113
161
|
}
|
|
114
162
|
|
|
115
163
|
/**
|
|
@@ -129,7 +177,7 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
129
177
|
* @returns the provider's exact argv and settlement-classification facts (a promise on dsh >= 0.1.6).
|
|
130
178
|
*/
|
|
131
179
|
confine(command, policy, signal) {
|
|
132
|
-
return this.ctx.sandbox.confine([this.
|
|
180
|
+
return this.ctx.sandbox.confine([this.requireBashPath(), '-c', command], policy, signal)
|
|
133
181
|
}
|
|
134
182
|
|
|
135
183
|
/**
|
|
@@ -219,7 +267,7 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
219
267
|
const routed = this.withParityEnv(spec)
|
|
220
268
|
const route = this.gitBashRoute(routed)
|
|
221
269
|
if (route === undefined) return super.execute(routed)
|
|
222
|
-
const argv = [this.
|
|
270
|
+
const argv = [this.requireBashPath(), '-c', routed.command]
|
|
223
271
|
if (route.unconfined) this.warnConfinedUnconfined(route.mode)
|
|
224
272
|
const sandbox = route.unconfined
|
|
225
273
|
? { mode: route.mode, denied: false, enforcement: 'unconfined' }
|
|
@@ -304,13 +352,13 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
304
352
|
const { mode } = policy
|
|
305
353
|
if (mode === 'danger-full-access') {
|
|
306
354
|
const { result } = GitBashSandboxExecutor.unwrapRunArgv(
|
|
307
|
-
await this.runArgv(spec, [this.
|
|
355
|
+
await this.runArgv(spec, [this.requireBashPath(), '-c', spec.command]))
|
|
308
356
|
return { ...result, sandbox: { mode, denied: false } }
|
|
309
357
|
}
|
|
310
358
|
if (process.platform === 'win32') {
|
|
311
359
|
this.warnConfinedUnconfined(mode)
|
|
312
360
|
const { result, spawnRequested } = GitBashSandboxExecutor.unwrapRunArgv(
|
|
313
|
-
await this.runArgv(spec, [this.
|
|
361
|
+
await this.runArgv(spec, [this.requireBashPath(), '-c', spec.command]))
|
|
314
362
|
// No spawn means no argv ran, so there is nothing to label as
|
|
315
363
|
// unconfined; the parent reports the same bare sandbox facts there.
|
|
316
364
|
return { ...result, sandbox: spawnRequested ? { mode, denied: false, enforcement: 'unconfined' } : { mode, denied: false } }
|
|
@@ -326,7 +374,7 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
326
374
|
const fullAccess = mode === 'danger-full-access'
|
|
327
375
|
if (fullAccess || process.platform === 'win32') {
|
|
328
376
|
if (!fullAccess) this.warnConfinedUnconfined(mode)
|
|
329
|
-
const proc = this.startArgv(spec, [this.
|
|
377
|
+
const proc = this.startArgv(spec, [this.requireBashPath(), '-c', spec.command])
|
|
330
378
|
proc.sandbox = fullAccess ? { mode, denied: false } : { mode, denied: false, enforcement: 'unconfined' }
|
|
331
379
|
// Match the loaded base contract: the async one (dsh >= 0.1.6) is
|
|
332
380
|
// awaited by its callers, the synchronous one is consumed directly.
|