dsh-gitbash-shell 0.34.3 → 0.35.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/src/index.js CHANGED
@@ -56,6 +56,8 @@ import { fileURLToPath } from 'node:url'
56
56
 
57
57
  import { PRESET_META, minimalPluginsFor, pluginsFor } from './compositions.js'
58
58
  import { loadSchemastery } from './schemastery.js'
59
+ import { GITBASH_MSYS_ADDENDUM, GITBASH_ROUTE_ADDENDUM, mountInlineTools, resolveHostPwsh7 } from './inline-tools.js'
60
+ import { mountFastctx } from './fastctx-bridge.js'
59
61
 
60
62
  /**
61
63
  * The executor base class's own Config schema, when this deployment has it.
@@ -119,6 +121,21 @@ const MARKER_FILE = '.plugin-managed.json'
119
121
  /** Default preset ids this plugin materializes, in roster order (configurable). */
120
122
  export const PRESET_IDS = ['standard-gitbash', 'minimal-gitbash', 'code-gitbash', 'cordis-gitbash']
121
123
 
124
+ /**
125
+ * Variant preset id -> the OFFICIAL successor id, for the deployment-default
126
+ * migration (v0.35.0). Every target is verified against the LIVE roster
127
+ * before any write — the official ids renamed once already (code→ptc), so a
128
+ * missing successor stands the migration down instead of writing an unknown
129
+ * id. `code-gitbash` deliberately maps to `ptc`: that variant has described
130
+ * the official PTC mode since dsh 0.1.2 renamed it.
131
+ */
132
+ export const VARIANT_DEFAULT_SUCCESSORS = Object.freeze({
133
+ 'standard-gitbash': 'standard',
134
+ 'minimal-gitbash': 'minimal',
135
+ 'code-gitbash': 'ptc',
136
+ 'cordis-gitbash': 'cordis',
137
+ })
138
+
122
139
  /**
123
140
  * The variant dsh-ptc-cordis-preset already covers while cooperating: its
124
141
  * `PTC 创造模式` is materialized against Git Bash too, so our own
@@ -474,6 +491,30 @@ export const Config = Schema === null ? undefined : Schema.object({
474
491
  * changing it on either side changes it here.
475
492
  */
476
493
  suppressPeerCordis: live(Schema.boolean().default(false)),
494
+ /**
495
+ * Serve the four `* · Git Bash` VARIANT presets (v0.34.x behaviour) instead
496
+ * of relying on the inline tool patch (v0.35.0). Default OFF for every user
497
+ * — new and existing — per the owner's explicit call: the official presets
498
+ * carry Git Bash through the per-agent tool patch, the roster stays the
499
+ * official four, and the variants remain one switch away for anyone who
500
+ * wants the duplicated roster back. OFF also stops the reconcile from
501
+ * registering variants; a session pinned to a variant id must re-select an
502
+ * official preset (the roster flip is announced in the CHANGELOG).
503
+ */
504
+ variantPresets: live(Schema.boolean().default(false)),
505
+ /**
506
+ * Publish `gitbash_pwsh` — a REAL PowerShell 7 tool over ctx.subprocess —
507
+ * while the host `pwsh` tool is denied (its executor is Git Bash on this
508
+ * host). Default ON; requires pwsh 7 on the machine (never a bundled
509
+ * payload) and danger-full-access per session.
510
+ */
511
+ pwshTool: live(Schema.boolean().default(true)),
512
+ /**
513
+ * Publish the FastCtx file tools (`gb_*`) through the local MCP bridge.
514
+ * Default ON; Windows-only payload, stands down entirely when dsh-ops is
515
+ * present (its ops_* tools already cover the same ground).
516
+ */
517
+ fastctxTools: live(Schema.boolean().default(true)),
477
518
  })
478
519
 
479
520
  /** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
@@ -533,6 +574,13 @@ function makeLiveReader(ctx, config) {
533
574
  adoptSidebar: valueOf(config.adoptSidebar) !== false,
534
575
  // Default ON (v0.29.0): the official terminal is ours unless opted out.
535
576
  autoTerminalShell: valueOf(config.autoTerminalShell) !== false,
577
+ // Default OFF (v0.35.0): the official presets carry Git Bash through the
578
+ // inline tool patch; the variant roster is opt-in.
579
+ variantPresets: valueOf(config.variantPresets) === true,
580
+ // Default ON (v0.35.0): the pwsh replacement fills the denied host tool.
581
+ pwshTool: valueOf(config.pwshTool) !== false,
582
+ // Default ON (v0.35.0): the FastCtx bridge stands down when dsh-ops is present.
583
+ fastctxTools: valueOf(config.fastctxTools) !== false,
536
584
  }
537
585
  }
538
586
  return {
@@ -540,6 +588,9 @@ function makeLiveReader(ctx, config) {
540
588
  posix: () => dialect().posixPaths === true,
541
589
  adoptSidebar: () => dialect().adoptSidebar !== false,
542
590
  autoTerminalShell: () => dialect().autoTerminalShell !== false,
591
+ variantPresets: () => (hasConfig ? valueOf(config.variantPresets) === true : false),
592
+ pwshTool: () => (hasConfig ? valueOf(config.pwshTool) !== false : true),
593
+ fastctxTools: () => (hasConfig ? valueOf(config.fastctxTools) !== false : true),
543
594
  // Default OFF: only an explicit true asks for deduplication.
544
595
  suppressPeerCordis: () => (hasConfig ? valueOf(config.suppressPeerCordis) === true : false),
545
596
  }
@@ -1742,7 +1793,7 @@ async function registerVariant(ctx, presetId, { gitBashActive, skillsDir, python
1742
1793
  * variant's PTY row reads it once per registration (issue #15).
1743
1794
  * @returns {Promise<void>} resolves once the first reconcile finished.
1744
1795
  */
1745
- async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolution) {
1796
+ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolution, readVariants) {
1746
1797
  cleanupLegacyTrees(presetIds)
1747
1798
  const gitBashActive = process.platform === 'win32'
1748
1799
  const skillsDir = await resolveSkillsDir()
@@ -1758,7 +1809,12 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
1758
1809
  let registeredPython = false
1759
1810
  let serial = Promise.resolve()
1760
1811
 
1761
- const desired = () => effectivePresetIds(presetIds, { suppress: readSuppress(), peerGitBash })
1812
+ // v0.35.0: the variant roster is opt-in (`variantPresets`, default OFF).
1813
+ // OFF means an EMPTY desired set — the cleanup above still runs, so a
1814
+ // legacy materialized tree never survives just because the switch is off.
1815
+ const desired = () => (readVariants && readVariants() === true
1816
+ ? effectivePresetIds(presetIds, { suppress: readSuppress(), peerGitBash })
1817
+ : [])
1762
1818
 
1763
1819
  const reconcile = () => {
1764
1820
  serial = serial.then(async () => {
@@ -1811,7 +1867,10 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
1811
1867
  }
1812
1868
 
1813
1869
  await reconcile()
1814
- console.log(TAG + ' registered ' + live.size + ' preset(s) declaratively (' + [...live.keys()].join(', ') + ')' + (hostExtras.timeContext || hostExtras.toolSchedule
1870
+ console.log(TAG + ' registered ' + live.size + ' preset(s) declaratively (' + [...live.keys()].join(', ') + ')'
1871
+ + (live.size === 0 ? ' — variantPresets is off: the official presets carry Git Bash through the inline tool patch (see the CHANGELOG)'
1872
+ : '')
1873
+ + (hostExtras.timeContext || hostExtras.toolSchedule
1815
1874
  ? ' [dsh 0.2.1 additions: ' + (hostExtras.timeContext ? 'time-context' : '') + (hostExtras.timeContext && hostExtras.toolSchedule ? ' + ' : '') + (hostExtras.toolSchedule ? 'tool-schedule (subagents denied)' : '') + ']'
1816
1875
  : ''))
1817
1876
 
@@ -1854,12 +1913,13 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
1854
1913
  console.log(TAG + ' peer capability wiring failed: ' + (error && error.message ? error.message : error))
1855
1914
  }
1856
1915
 
1857
- // The switch itself: a volatile Config edit on this row lands as a
1858
- // loader/volatile-update event on this fiber.
1916
+ // The switches themselves: a volatile Config edit on this row lands as a
1917
+ // loader/volatile-update event on this fiber (v0.35.0 adds variantPresets).
1859
1918
  try {
1860
1919
  ctx.on('loader/volatile-update', (paths) => {
1861
1920
  try {
1862
- if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && path[0] === 'suppressPeerCordis')) return
1921
+ if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path)
1922
+ && (path[0] === 'suppressPeerCordis' || path[0] === 'variantPresets'))) return
1863
1923
  void reconcile()
1864
1924
  } catch (error) {
1865
1925
  console.log(TAG + ' dedupe switch handling failed: ' + (error && error.message ? error.message : error))
@@ -1921,6 +1981,11 @@ export async function apply(ctx, config = {}) {
1921
1981
  const readConfiguredBashPath = () => effectiveConfiguredBashPath(executorConfiguredBashPath(ctx), rowBashPath(config))
1922
1982
  const readBashResolution = () => resolveGitBashCached({ configured: readConfiguredBashPath() })
1923
1983
  const gitBashCapability = {
1984
+ // v0.35.0 semantics, unchanged shape: `active` answers "does command
1985
+ // execution run on Git Bash on this host" — TRUE regardless of the
1986
+ // variantPresets switch, because the inline tool patch carries the
1987
+ // official presets too. Peers (dsh-ptc-cordis-preset) key their roster
1988
+ // wording and dedupe on exactly this fact.
1924
1989
  active: process.platform === 'win32',
1925
1990
  // Kept for existing consumers (peer plugins read `bashPath`): the resolved
1926
1991
  // interpreter. When resolution failed we report the explicit value (or the
@@ -2015,6 +2080,21 @@ export async function apply(ctx, config = {}) {
2015
2080
  no run_code hint — so they meet the shell they were composed for. */
2016
2081
  const dialect = liveSettings.dialect()
2017
2082
  const applies = dialectApplies(dialect, assembleContext && assembleContext.agent)
2083
+ // v0.35.0: shell-routing addendum on the `bash` tool's description.
2084
+ // Description is schema-level text, so even the minimal preset's
2085
+ // complete-persona cannot strip it; the ROUTING sentence applies to
2086
+ // every agent (routing is dialect-independent), while the MSYS
2087
+ // sentence only describes the world the dialect switch creates.
2088
+ // Idempotent by marker: assemble may run more than once per agent.
2089
+ if (assembly && typeof assembly === 'object' && Array.isArray(assembly.tools)) {
2090
+ for (const tool of assembly.tools) {
2091
+ if (tool && tool.name === 'bash' && typeof tool.description === 'string'
2092
+ && !tool.description.includes('Prefer this bash tool')) {
2093
+ tool.description += '\n' + GITBASH_ROUTE_ADDENDUM
2094
+ + (applies && dialect.posixPaths === true ? '\n' + GITBASH_MSYS_ADDENDUM : '')
2095
+ }
2096
+ }
2097
+ }
2018
2098
  if (applies && dialect.posixPaths === true && assembly && typeof assembly === 'object') {
2019
2099
  for (const section of Array.isArray(assembly.sections) ? assembly.sections : []) {
2020
2100
  if (section && typeof section.text === 'string') section.text = windowsToMsys(section.text)
@@ -2509,13 +2589,149 @@ export async function apply(ctx, config = {}) {
2509
2589
  })
2510
2590
  }
2511
2591
 
2592
+ // ── inline tool patch (v0.35.0): Git Bash for the OFFICIAL presets ──────
2593
+ // The official preset rows compose `tool-bash` as win32-disabled, and the
2594
+ // patch layer cannot reach rows nested in a preset declaration (buildMap
2595
+ // only descends into group rows), so the fix runs at the tool layer: per
2596
+ // agent, a face of "pwsh yes / bash no" gets the OFFICIAL tool-bash
2597
+ // activated in an agent-owned scope plus a deny on the fake pwsh (its
2598
+ // executor is ctx.shell, which here IS Git Bash), and the assemble
2599
+ // waterfall carries the bash-first routing sentence. The variantPresets
2600
+ // switch is honored BY OBSERVATION — a variant agent's face already has
2601
+ // bash, so this patch never fires there.
2602
+ if (process.platform === 'win32') {
2603
+ try {
2604
+ mountInlineTools(ctx, {
2605
+ tools: (c) => { try { return c.get('tools') } catch { return undefined } },
2606
+ createScope: (agentCtx) => {
2607
+ let childCtx
2608
+ const fiber = agentCtx.plugin((c) => { childCtx = c })
2609
+ return {
2610
+ ctx: childCtx,
2611
+ dispose: async () => { try { await fiber?.dispose?.() } catch { /* already gone */ } },
2612
+ }
2613
+ },
2614
+ allowed: (session) => {
2615
+ try { return ctx.get('sandboxPolicy')?.resolve({ session })?.mode === 'danger-full-access' } catch { return false }
2616
+ },
2617
+ spawn: (spec) => {
2618
+ const subprocess = ctx.get('subprocess')
2619
+ if (!subprocess || typeof subprocess.spawn !== 'function') throw new Error('the host subprocess service is not available on this deployment')
2620
+ return subprocess.spawn(spec)
2621
+ },
2622
+ pwshPath: resolveHostPwsh7(),
2623
+ pwshEnabled: () => liveSettings.pwshTool(),
2624
+ msysToWin: (p) => translateMsysPath(p, buildTranslateEnv(readConfiguredBashPath())),
2625
+ echo: (text) => (liveSettings.dialect().posixPaths === true ? windowsToMsys(text) : text),
2626
+ log: (m) => console.log(TAG + ' ' + m),
2627
+ })
2628
+ console.log(TAG + ' inline tool patch armed (official presets get Git Bash; bash-first routing enforced)')
2629
+ } catch (error) {
2630
+ console.log(TAG + ' inline tool patch wiring failed: ' + (error?.message ?? error))
2631
+ }
2632
+ }
2633
+
2634
+ // ── default-preset migration (v0.35.0, user request) ─────────────────────
2635
+ // A session records its preset by ID (`agent-preset/selected`), and the
2636
+ // registry resolves EVERY mount through `definitions.get(id)` — a variant
2637
+ // id that is not registered fails `agent-preset/not-found`, and the
2638
+ // official `select` refuses mid-session switches (`agent-preset/locked`),
2639
+ // so PINNED SESSIONS cannot be migrated by any official API. The DEPLOYMENT
2640
+ // DEFAULT, however, is one writable setting: `selectedDefault` on the
2641
+ // `agent-preset-registry` row (volatile; the official preset picker writes
2642
+ // it through `settings.update('agent-preset-registry', ...)`). While
2643
+ // variantPresets is OFF (the user did NOT keep the old modes) and that
2644
+ // default still names one of OUR variants, migrate it to the official
2645
+ // successor once — idempotent (a migrated or user-chosen official default
2646
+ // never matches PRESET_IDS again), roster-checked (the official id renamed
2647
+ // once already: code→ptc — a missing target stands down instead of
2648
+ // guessing), written through the OFFICIAL settings seam, and verified by
2649
+ // read-back (a write that did not stick is reported, never claimed).
2650
+ // With variantPresets ON nothing here runs: the variants stay registered,
2651
+ // so a variant default keeps working as-is.
2652
+ try {
2653
+ if (ctx.agentPresets && typeof ctx.agentPresets.register === 'function') {
2654
+ ctx.inject(['settings'], (settingsCtx) => {
2655
+ void (async () => {
2656
+ try {
2657
+ const settings = settingsCtx.settings
2658
+ if (!settings || typeof settings.update !== 'function') return
2659
+ // The switch is the whole contract: keep-old-modes ON means a
2660
+ // variant default is a legitimate choice and stays untouched.
2661
+ if (liveSettings.variantPresets() === true) return
2662
+ const readDefault = () => { try { return ctx.agentPresets.defaultId } catch { return undefined } }
2663
+ const current = readDefault()
2664
+ if (typeof current !== 'string' || !PRESET_IDS.includes(current)) return
2665
+ const target = VARIANT_DEFAULT_SUCCESSORS[current]
2666
+ if (typeof target !== 'string') return
2667
+ // The successor must exist in the CURRENT roster: official ids
2668
+ // renamed once already (code→ptc), and writing an unknown id
2669
+ // would trade a known failure for a worse one.
2670
+ const roster = await ctx.agentPresets.list()
2671
+ if (!Array.isArray(roster) || !roster.some((row) => row && row.id === target)) {
2672
+ console.warn(TAG + ' default-preset migration stood down: the deployment default is the variant "'
2673
+ + current + '" but the successor "' + target + '" is not registered on this host.'
2674
+ + ' Set the default manually among the OFFICIAL presets, or turn variantPresets on.')
2675
+ return
2676
+ }
2677
+ try {
2678
+ await settings.update('agent-preset-registry', { selectedDefault: target })
2679
+ } catch (error) {
2680
+ console.warn(TAG + ' default-preset migration write failed (' + (error?.message ?? error)
2681
+ + '): the deployment default is still the variant "' + current
2682
+ + '" — NEW sessions would fail with agent-preset/not-found.'
2683
+ + ' Fix: set an OFFICIAL default in the preset picker, or turn variantPresets on.')
2684
+ return
2685
+ }
2686
+ const after = readDefault()
2687
+ if (after === target) {
2688
+ console.log(TAG + ' deployment default migrated: "' + current + '" -> "' + target
2689
+ + '" (variantPresets is off). Old sessions pinned to a variant still need a NEW'
2690
+ + ' session — the official agent-preset/locked rule blocks a mid-session switch.')
2691
+ } else {
2692
+ console.warn(TAG + ' default-preset migration did not stick (read back "'
2693
+ + (after === undefined ? 'unreadable' : after) + '"). Set the default manually in'
2694
+ + ' Settings -> agent preset, or turn variantPresets on.')
2695
+ }
2696
+ } catch (error) {
2697
+ console.log(TAG + ' default-preset migration failed: ' + (error?.message ?? error))
2698
+ }
2699
+ })()
2700
+ })
2701
+ }
2702
+ } catch (error) {
2703
+ console.log(TAG + ' default-preset migration wiring failed: ' + (error?.message ?? error))
2704
+ }
2705
+
2706
+ // ── FastCtx bridge (v0.35.0): the gb_* file tools, default ON ────────────
2707
+ // Additions, never replacements: the official read/grep/glob/edit tools and
2708
+ // their file sandbox stay first choice in every description. The bridge
2709
+ // mounts nothing outside win32, nothing when the payload is absent, and
2710
+ // stands down entirely the moment dsh-ops (ops_* tools) is detected.
2711
+ if (process.platform === 'win32') {
2712
+ try {
2713
+ const { createRequire } = await import('node:module')
2714
+ mountFastctx(ctx, {
2715
+ platform: process.platform,
2716
+ enabled: () => liveSettings.fastctxTools(),
2717
+ createRequireFrom: (base) => createRequire(base ?? import.meta.url),
2718
+ tools: (c) => { try { return c.get('tools') } catch { return undefined } },
2719
+ msysToWin: (p) => translateMsysPath(p, buildTranslateEnv(readConfiguredBashPath())),
2720
+ echo: (text) => (liveSettings.dialect().posixPaths === true ? windowsToMsys(text) : text),
2721
+ log: (m) => console.log(TAG + ' ' + m),
2722
+ })
2723
+ } catch (error) {
2724
+ console.log(TAG + ' FastCtx bridge wiring failed: ' + (error?.message ?? error))
2725
+ }
2726
+ }
2727
+
2512
2728
  // ── era split: declarative registration on dsh >= 0.1.7 ──────────────────
2513
2729
  // The register method IS the era signal. On the new host the whole
2514
2730
  // materialization path below is dead code (nothing reads the directory any
2515
2731
  // more), so this branch serves the variants and returns.
2516
2732
  if (ctx.agentPresets && typeof ctx.agentPresets.register === 'function') {
2517
2733
  try {
2518
- await runDeclarativeEra(ctx, presetIds, () => liveSettings.suppressPeerCordis(), readBashResolution)
2734
+ await runDeclarativeEra(ctx, presetIds, () => liveSettings.suppressPeerCordis(), readBashResolution, () => liveSettings.variantPresets())
2519
2735
  } catch (error) {
2520
2736
  console.log(TAG + ' declarative registration failed: ' + (error && error.message ? error.message : error))
2521
2737
  }
@@ -2524,4 +2740,4 @@ export async function apply(ctx, config = {}) {
2524
2740
 
2525
2741
  }
2526
2742
  // Test surface: pure helpers, no Cordis context required.
2527
- export const _internal = { PRESET_IDS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, translateDispatch, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, hashTree, installRegisterShim, probeHostExtras }
2743
+ export const _internal = { PRESET_IDS, VARIANT_DEFAULT_SUCCESSORS, PEER_COVERED_PRESET_ID, PEER_CAPABILITY, isDelegatedAgent, dialectApplies, PEER_PYTHON_FIELD, PEER_PYTHON_BACKEND_FIELD, peerFact, peerBackend, pythonBackendActive, effectivePresetIds, translateDispatch, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, windowsToMsys, rewriteResultPaths, adoptSidebarShell, MARKER_FILE, classify, hashTree, installRegisterShim, probeHostExtras }