dsh-gitbash-shell 0.26.1 → 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 +74 -0
- package/README.md +37 -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 +407 -7
- package/src/index.js +178 -31
- package/src/shell.js +58 -10
package/src/index.js
CHANGED
|
@@ -180,6 +180,51 @@ export function pythonBackendActive(capability) {
|
|
|
180
180
|
return intent && reported === 'python'
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Is this agent a DELEGATED one — a subagent, a team member, or a nested child
|
|
185
|
+
* of either? The canonical in-tree signal is the session header pair dsh's own
|
|
186
|
+
* workspace-changes plugin uses
|
|
187
|
+
* (`packages/deliverables/workspace-changes/src/index.ts:59-60`), stamped when
|
|
188
|
+
* the subagent driver creates a child
|
|
189
|
+
* (`packages/subagent/subagent/src/child-agent.ts:139-155`):
|
|
190
|
+
* `origin === 'subagent'` or a non-zero `delegationDepth`.
|
|
191
|
+
*
|
|
192
|
+
* Unknown shapes read as NOT delegated: applying the dialect is the helpful
|
|
193
|
+
* direction, and a diagnostic assembly without an agent must keep it.
|
|
194
|
+
* @param {object|undefined} agent - the assembling/executing agent, when known.
|
|
195
|
+
* @returns {boolean} true only when the agent is provably delegated.
|
|
196
|
+
*/
|
|
197
|
+
export function isDelegatedAgent(agent) {
|
|
198
|
+
try {
|
|
199
|
+
const header = agent && agent.session ? agent.session.header : undefined
|
|
200
|
+
if (header === null || typeof header !== 'object') return false
|
|
201
|
+
if (header.origin === 'subagent') return true
|
|
202
|
+
return typeof header.delegationDepth === 'number' && header.delegationDepth > 0
|
|
203
|
+
} catch {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Does the Git Bash dialect apply to THIS agent (v0.27.0)? The row switch
|
|
210
|
+
* `subagentDialect` (default ON) decides whether delegated agents — subagents,
|
|
211
|
+
* team members, nested children — participate in the dialect at all; the main
|
|
212
|
+
* agent always does. A closed function so every consumer (prompt assembly,
|
|
213
|
+
* dispatch translation, result echo, failure text, the shell-env fact) answers
|
|
214
|
+
* the same way.
|
|
215
|
+
*
|
|
216
|
+
* Note the honest bound: dsh has exactly ONE shell provider per process, so
|
|
217
|
+
* the executor binary stays Git Bash for every agent; this gate governs the
|
|
218
|
+
* dialect/translation layers that this plugin owns.
|
|
219
|
+
* @param {object} dialect - the live dialect switches.
|
|
220
|
+
* @param {object|undefined} agent - the agent that owns this assembly/execution.
|
|
221
|
+
* @returns {boolean} true when the dialect applies to this agent.
|
|
222
|
+
*/
|
|
223
|
+
export function dialectApplies(dialect, agent) {
|
|
224
|
+
if (!dialect || dialect.subagentDialect !== false) return true
|
|
225
|
+
return !isDelegatedAgent(agent)
|
|
226
|
+
}
|
|
227
|
+
|
|
183
228
|
/**
|
|
184
229
|
* The roster this plugin serves: the configured list, minus the variant a peer
|
|
185
230
|
* already covers. Two independent facts must BOTH hold before anything is
|
|
@@ -200,8 +245,15 @@ export function effectivePresetIds(configured, { suppress, peerGitBash } = {}) {
|
|
|
200
245
|
return list.filter((id) => id !== PEER_COVERED_PRESET_ID)
|
|
201
246
|
}
|
|
202
247
|
|
|
203
|
-
/**
|
|
204
|
-
|
|
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'
|
|
205
257
|
|
|
206
258
|
/** The shipped preset whose skills/ dir seeds cordis-gitbash. */
|
|
207
259
|
const SKILLS_SOURCE_PRESET = 'cordis'
|
|
@@ -823,6 +875,15 @@ export const Config = Schema === null ? undefined : Schema.object({
|
|
|
823
875
|
globSplit: live(Schema.boolean().default(true)),
|
|
824
876
|
errorDialect: live(Schema.boolean().default(true)),
|
|
825
877
|
codePaths: live(Schema.boolean().default(true)),
|
|
878
|
+
/**
|
|
879
|
+
* Whether DELEGATED agents (subagents, team members, nested children) also
|
|
880
|
+
* use the Git Bash dialect (v0.27.0). Default ON — the historical behavior;
|
|
881
|
+
* OFF keeps the dialect to the main agent, so a delegated request carries the
|
|
882
|
+
* official shell semantics (no prompt rewrite, no path-argument translation,
|
|
883
|
+
* no echo rewrite, no DSH_PATH_DIALECT fact). The executor itself is one
|
|
884
|
+
* per-process provider, so Git Bash stays the binary either way.
|
|
885
|
+
*/
|
|
886
|
+
subagentDialect: live(Schema.boolean().default(true)),
|
|
826
887
|
gitAutocrlf: live(Schema.boolean().default(true)),
|
|
827
888
|
bashPath: live(Schema.string().default('')),
|
|
828
889
|
adoptSidebar: live(Schema.boolean().default(true)),
|
|
@@ -869,6 +930,8 @@ function makeLiveReader(ctx, config) {
|
|
|
869
930
|
errorDialect: valueOf(config.errorDialect) === true,
|
|
870
931
|
codePaths: valueOf(config.codePaths) === true,
|
|
871
932
|
gitAutocrlf: valueOf(config.gitAutocrlf) === true,
|
|
933
|
+
// Default ON: delegated agents keep the dialect unless the user opts out.
|
|
934
|
+
subagentDialect: valueOf(config.subagentDialect) !== false,
|
|
872
935
|
bashPath: typeof bash === 'string' ? bash : '',
|
|
873
936
|
adoptSidebar: valueOf(config.adoptSidebar) !== false,
|
|
874
937
|
}
|
|
@@ -943,7 +1006,7 @@ export function readPosixPaths(ctxLike) {
|
|
|
943
1006
|
* first. All default ON (schema defaults); never throws.
|
|
944
1007
|
*/
|
|
945
1008
|
export function readDialectSettings(ctxLike) {
|
|
946
|
-
const fallback = { posixPaths: false, virtualMounts: false, globSplit: false, errorDialect: false, codePaths: false, gitAutocrlf: false, bashPath: '' }
|
|
1009
|
+
const fallback = { posixPaths: false, virtualMounts: false, globSplit: false, errorDialect: false, codePaths: false, gitAutocrlf: false, bashPath: '', subagentDialect: true }
|
|
947
1010
|
try {
|
|
948
1011
|
const settings = ctxLike && typeof ctxLike.get === 'function' ? ctxLike.get('settings') : undefined
|
|
949
1012
|
const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
|
|
@@ -954,6 +1017,8 @@ export function readDialectSettings(ctxLike) {
|
|
|
954
1017
|
globSplit: v.globSplit === true,
|
|
955
1018
|
errorDialect: v.errorDialect === true,
|
|
956
1019
|
codePaths: v.codePaths === true,
|
|
1020
|
+
// An old host without the key keeps the default (delegated agents covered).
|
|
1021
|
+
subagentDialect: v.subagentDialect !== false,
|
|
957
1022
|
gitAutocrlf: v.gitAutocrlf === true,
|
|
958
1023
|
bashPath: typeof v.bashPath === 'string' ? v.bashPath : '',
|
|
959
1024
|
}
|
|
@@ -1390,23 +1455,12 @@ export function buildTranslateEnv(configuredBashPath) {
|
|
|
1390
1455
|
if (h && existsSync(h)) env.home = h.replace(/\\/g, '/')
|
|
1391
1456
|
} catch { /* keep undefined */ }
|
|
1392
1457
|
try {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
}
|
|
1400
|
-
for (const candidate of candidates) {
|
|
1401
|
-
try {
|
|
1402
|
-
if (!existsSync(candidate)) continue
|
|
1403
|
-
const root = dirname(dirname(candidate))
|
|
1404
|
-
if (existsSync(join(root, 'usr', 'bin'))) {
|
|
1405
|
-
env.gitRoot = root.replace(/\\/g, '/')
|
|
1406
|
-
break
|
|
1407
|
-
}
|
|
1408
|
-
} catch { /* try the next candidate */ }
|
|
1409
|
-
}
|
|
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, '/')
|
|
1410
1464
|
} catch { /* keep undefined */ }
|
|
1411
1465
|
translateEnvCache.set(cacheKey, env)
|
|
1412
1466
|
return env
|
|
@@ -2293,7 +2347,28 @@ export async function apply(ctx, config = {}) {
|
|
|
2293
2347
|
// plugins (e.g. dsh-ptc-cordis-preset) can adopt it in the presets they
|
|
2294
2348
|
// materialize: service present, 'active: true' on Windows, 'active: false'
|
|
2295
2349
|
// where the bundle is installed but the platform stack stayed native.
|
|
2296
|
-
|
|
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
|
+
}
|
|
2297
2372
|
const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
|
|
2298
2373
|
ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
|
|
2299
2374
|
|
|
@@ -2336,6 +2411,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2336
2411
|
globSplit: Schema.boolean().default(true),
|
|
2337
2412
|
errorDialect: Schema.boolean().default(true),
|
|
2338
2413
|
codePaths: Schema.boolean().default(true),
|
|
2414
|
+
subagentDialect: Schema.boolean().default(true),
|
|
2339
2415
|
gitAutocrlf: Schema.boolean().default(true),
|
|
2340
2416
|
bashPath: Schema.string().default(''),
|
|
2341
2417
|
adoptSidebar: Schema.boolean().default(true),
|
|
@@ -2391,8 +2467,17 @@ export async function apply(ctx, config = {}) {
|
|
|
2391
2467
|
// git line endings and anything else non-DSH_* ride the executor's
|
|
2392
2468
|
// own env layer (src/shell.js), not this registry.
|
|
2393
2469
|
variables: { [PATH_DIALECT_KEY]: { description: PATH_DIALECT_DESCRIPTION } },
|
|
2394
|
-
resolve() {
|
|
2395
|
-
|
|
2470
|
+
resolve(execution) {
|
|
2471
|
+
try {
|
|
2472
|
+
const dialect = liveSettings.dialect()
|
|
2473
|
+
if (dialect.posixPaths !== true) return {}
|
|
2474
|
+
// The environment fact is per execution, so a delegated agent
|
|
2475
|
+
// the user excluded sees no DSH_PATH_DIALECT at all.
|
|
2476
|
+
if (!dialectApplies(dialect, execution && execution.agent)) return {}
|
|
2477
|
+
return { [PATH_DIALECT_KEY]: PATH_DIALECT_VALUE }
|
|
2478
|
+
} catch {
|
|
2479
|
+
return {}
|
|
2480
|
+
}
|
|
2396
2481
|
},
|
|
2397
2482
|
})
|
|
2398
2483
|
envCtx.effect(() => unregister, 'dsh-gitbash-shell: shellEnv path-dialect fact')
|
|
@@ -2418,9 +2503,16 @@ export async function apply(ctx, config = {}) {
|
|
|
2418
2503
|
// could corrupt pattern/default fields).
|
|
2419
2504
|
if (process.platform === 'win32') {
|
|
2420
2505
|
try {
|
|
2421
|
-
ctx.on('system-prompt/assemble', (assembly,
|
|
2506
|
+
ctx.on('system-prompt/assemble', (assembly, assembleContext, next) => {
|
|
2422
2507
|
try {
|
|
2423
|
-
|
|
2508
|
+
/* v0.27.0: the main agent always participates; a DELEGATED agent
|
|
2509
|
+
(subagent / team member / nested child) only while the
|
|
2510
|
+
`subagentDialect` switch is on. Turning it off leaves the official
|
|
2511
|
+
prompt text untouched for those agents — no source replacement and
|
|
2512
|
+
no run_code hint — so they meet the shell they were composed for. */
|
|
2513
|
+
const dialect = liveSettings.dialect()
|
|
2514
|
+
const applies = dialectApplies(dialect, assembleContext && assembleContext.agent)
|
|
2515
|
+
if (applies && dialect.posixPaths === true && assembly && typeof assembly === 'object') {
|
|
2424
2516
|
for (const section of Array.isArray(assembly.sections) ? assembly.sections : []) {
|
|
2425
2517
|
if (section && typeof section.text === 'string') section.text = windowsToMsys(section.text)
|
|
2426
2518
|
}
|
|
@@ -2491,9 +2583,12 @@ export async function apply(ctx, config = {}) {
|
|
|
2491
2583
|
// `bash.workdir` went through raw, and an in-workspace `/c/...` write
|
|
2492
2584
|
// fell outside the sandbox. One missed call site, five symptoms.
|
|
2493
2585
|
const dialect = liveSettings.dialect()
|
|
2494
|
-
|
|
2586
|
+
// v0.27.0: a delegated agent the user excluded keeps the official
|
|
2587
|
+
// semantics for BOTH faces of its calls — arguments go through
|
|
2588
|
+
// untouched and the result metadata is not echoed back in MSYS form.
|
|
2589
|
+
if (dialect.posixPaths && dialectApplies(dialect, exec && exec.agent)) {
|
|
2495
2590
|
echo = true
|
|
2496
|
-
const env = dialect.virtualMounts ? buildTranslateEnv(
|
|
2591
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(configuredBashPath) : null
|
|
2497
2592
|
echoEnv = env
|
|
2498
2593
|
if (exec && exec.arguments && typeof exec.arguments === 'object') {
|
|
2499
2594
|
const translated = translateDispatch(exec, dialect, env)
|
|
@@ -2542,9 +2637,13 @@ export async function apply(ctx, config = {}) {
|
|
|
2542
2637
|
let contentPatch
|
|
2543
2638
|
try {
|
|
2544
2639
|
const dialect = liveSettings.dialect()
|
|
2640
|
+
// Same agent gate as the dispatch wrapper: one call, one dialect.
|
|
2641
|
+
// RETURN the downstream promise: a waterfall listener that swallows
|
|
2642
|
+
// it would hand the caller `undefined` instead of the decision.
|
|
2643
|
+
if (!dialectApplies(dialect, exec && exec.agent)) return next()
|
|
2545
2644
|
if (dialect.posixPaths && dialect.errorDialect && exec && result && typeof result === 'object'
|
|
2546
2645
|
&& result.isError === true && Array.isArray(result.content)) {
|
|
2547
|
-
const env = dialect.virtualMounts ? buildTranslateEnv(
|
|
2646
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(configuredBashPath) : null
|
|
2548
2647
|
if (exec.name === 'run_code') {
|
|
2549
2648
|
// paths only — the /dev/null hint is for file tools
|
|
2550
2649
|
const content = rewriteErrorContent(result.content, { nulHint: false, env })
|
|
@@ -2588,10 +2687,14 @@ export async function apply(ctx, config = {}) {
|
|
|
2588
2687
|
pctx.effect(() => pctx.systemPrompt.context({
|
|
2589
2688
|
name: 'gitbash-shell:posix-paths',
|
|
2590
2689
|
order: 126,
|
|
2591
|
-
text: () => {
|
|
2690
|
+
text: (assembleContext) => {
|
|
2592
2691
|
try {
|
|
2593
2692
|
const dialect = liveSettings.dialect()
|
|
2594
2693
|
if (dialect.posixPaths !== true) return ''
|
|
2694
|
+
// A delegated agent whose dialect the user turned off meets the
|
|
2695
|
+
// official shell semantics: no directive, the same way no
|
|
2696
|
+
// translation happens for its calls.
|
|
2697
|
+
if (!dialectApplies(dialect, assembleContext && assembleContext.agent)) return ''
|
|
2595
2698
|
return dialect.virtualMounts === false ? POSIX_DIRECTIVE_TEXT_STRICT : POSIX_DIRECTIVE_TEXT
|
|
2596
2699
|
} catch {
|
|
2597
2700
|
return ''
|
|
@@ -2608,12 +2711,56 @@ export async function apply(ctx, config = {}) {
|
|
|
2608
2711
|
}
|
|
2609
2712
|
}
|
|
2610
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
|
+
|
|
2611
2757
|
// ── dsh-better-sidebar terminal adoption (Windows only) ────────────────
|
|
2612
2758
|
// The sidebar resolves its terminal shell through the settings seam per
|
|
2613
2759
|
// open; adopt it through that seam (see adoptSidebarShell for rationale).
|
|
2614
2760
|
// Disable with `betterSidebarShell: false` in the plugin row config.
|
|
2615
2761
|
if (config.betterSidebarShell !== false && process.platform === 'win32') {
|
|
2616
|
-
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)')
|
|
2617
2764
|
}
|
|
2618
2765
|
|
|
2619
2766
|
// ── era split: declarative registration on dsh >= 0.1.7 ──────────────────
|
|
@@ -2717,4 +2864,4 @@ export async function apply(ctx, config = {}) {
|
|
|
2717
2864
|
}
|
|
2718
2865
|
|
|
2719
2866
|
// Test surface: pure helpers, no Cordis context required.
|
|
2720
|
-
export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, detectPeerCoverage, readSuppressPeerCordis, translateDispatch, 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 }
|
|
2867
|
+
export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, detectPeerCoverage, readSuppressPeerCordis, translateDispatch, 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
|
@@ -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.
|