dsh-gitbash-shell 0.22.0 → 0.23.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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.22.0",
3
+ "version": "0.23.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.22.0",
3
+ "version": "0.23.0",
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": {
package/src/index.js CHANGED
@@ -756,13 +756,17 @@ export function readAdoptSidebar(ctxLike) {
756
756
  }
757
757
  }
758
758
 
759
- // Windows absolute path -> MSYS drive-root form, for rewriting the OFFICIAL
760
- // prompt text in place (v0.10.0). Two patterns keep quoted spaced paths
761
- // ("C:\Program Files\Git") whole while bare paths stop at whitespace or a
762
- // closing punctuation; the lookbehind set rejects URL schemes (https:),
763
- // file:// forms, and anything already mid-word, so only real drive-letter
764
- // paths are translated. Separators normalize to single slashes.
765
- const BARE_WIN_PATH = /(?<![A-Za-z0-9:\\/"'`])([A-Za-z]):(?:\\|\/)([^\s"'`<>|),;:!?]+)/g
759
+ // Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
760
+ // (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
761
+ // path stops at a closing punctuation and crosses a space ONLY into a token that
762
+ // itself carries a separator — so "C:\Program Files\Git\bin" comes back whole (v0.23.0)
763
+ // while "see C:\Users\kanna for details" keeps its prose. An unquoted path whose LAST
764
+ // segment holds the space (…\my dir\f.txt") stays ambiguous in prose; the model meets such
765
+ // a path as a whole-value FIELD, which never goes through here (driveToMsys).
766
+ // The lookbehind set rejects URL schemes (https:), file:// forms, and anything
767
+ // already mid-word, so only real drive-letter paths are translated. Separators
768
+ // normalize to single slashes.
769
+ const BARE_WIN_PATH = /(?<![A-Za-z0-9:\\/"'`])([A-Za-z]):(?:\\|\/)([^\s"'`<>|),;:!?]+(?: [^\s"'`<>|),;:!?]*[\\/][^\s"'`<>|),;:!?]*)*)/g
766
770
  const QUOTED_WIN_PATH = /(["'`])([A-Za-z]):(?:\\|\/)([^`]*?)\1/g
767
771
 
768
772
  /** Rewrite every Windows absolute path in a text to the MSYS form; pure. */
@@ -785,7 +789,7 @@ export function windowsToMsys(text) {
785
789
  function msysEcho(value, env) {
786
790
  const out = windowsToMsys(value)
787
791
  if (typeof out !== 'string' || out.length === 0) return out
788
- const tempRoot = env && env.tmpDir ? windowsToMsys(env.tmpDir) : ''
792
+ const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
789
793
  if (!tempRoot) return out
790
794
  return mountTempRoot(out, tempRoot)
791
795
  }
@@ -829,6 +833,38 @@ function posixSeparators(value) {
829
833
  : value
830
834
  }
831
835
 
836
+ /**
837
+ * One WHOLE path value -> the MSYS dialect (v0.23.0). windowsToMsys is a PROSE
838
+ * rewriter: its bare pattern stops at whitespace, so a path whose DIRECTORY
839
+ * contains a space came back half-translated — "C:\my dir\f.txt" turned into
840
+ * "/c/my dir\f.txt", and the model met that mixed form in a SUCCESSFUL
841
+ * read/write/edit result (issue #3). A whole-value field IS exactly one path, so
842
+ * the drive prefix is translated directly and every remaining separator
843
+ * normalizes; no prose pattern is involved. Relative values (grep match paths)
844
+ * only normalize. A UNC root becomes the MSYS spelling "//server/share";
845
+ * anything without a drive prefix passes through unchanged. Pure.
846
+ */
847
+ function driveToMsys(value) {
848
+ if (typeof value !== 'string' || value.length === 0) return value
849
+ const drive = /^([A-Za-z]):(?=[\\/])/.exec(value)
850
+ if (drive) return '/' + drive[1].toLowerCase() + '/' + posixSeparators(value.slice(2)).replace(/^\/+/, '')
851
+ return posixSeparators(value)
852
+ }
853
+
854
+ /**
855
+ * The MSYS echo of one whole path VALUE — never prose (v0.23.0): the drive root
856
+ * and separators through driveToMsys, then the /tmp mount. Every whole-value
857
+ * face (read/write/edit path, glob paths[], grep matches[].path, present
858
+ * files[].path) goes through here, so none of them can disagree about the
859
+ * dialect, and a spaced directory no longer leaks a backslash.
860
+ */
861
+ function pathEcho(value, env) {
862
+ const out = driveToMsys(value)
863
+ if (typeof out !== 'string' || out.length === 0) return out
864
+ const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
865
+ return tempRoot ? mountTempRoot(out, tempRoot) : out
866
+ }
867
+
832
868
  /**
833
869
  * Rewrite Windows absolute paths inside an ERROR result's text blocks to
834
870
  * the MSYS dialect (pure, v0.17.1). Error results cannot carry a
@@ -918,7 +954,11 @@ export function rewriteResultPaths(name, value, env) {
918
954
  // v0.17.0: paths under the user TEMP directory echo back in the bash
919
955
  // /tmp dialect (that IS what /tmp means in Git Bash), so the model sees
920
956
  // the same short root bash itself prints for $TMP.
921
- const toMsys = (p) => msysEcho(p, env)
957
+ // v0.23.0: every value around here is ONE path, so it goes through
958
+ // pathEcho — the prose rewriter stops at whitespace and left a spaced
959
+ // directory half-translated (issue #3). posixSeparators now lives inside
960
+ // pathEcho, so the three nested branches below need no second pass.
961
+ const toMsys = (p) => pathEcho(p, env)
922
962
  if (name === 'read' || name === 'read_image' || name === 'write' || name === 'edit') {
923
963
  if (typeof value.path === 'string' && value.path !== '') {
924
964
  const out = toMsys(value.path)
@@ -930,7 +970,7 @@ export function rewriteResultPaths(name, value, env) {
930
970
  let changed = false
931
971
  const files = value.files.map((file) => {
932
972
  if (!file || typeof file !== 'object' || typeof file.path !== 'string' || file.path === '') return file
933
- const out = posixSeparators(toMsys(file.path))
973
+ const out = toMsys(file.path)
934
974
  if (out === file.path) return file
935
975
  changed = true
936
976
  return { ...file, path: out }
@@ -941,7 +981,7 @@ export function rewriteResultPaths(name, value, env) {
941
981
  let changed = false
942
982
  const paths = value.paths.map((p) => {
943
983
  if (typeof p !== 'string') return p
944
- const out = posixSeparators(toMsys(p))
984
+ const out = toMsys(p)
945
985
  if (out === p) return p
946
986
  changed = true
947
987
  return out
@@ -952,7 +992,7 @@ export function rewriteResultPaths(name, value, env) {
952
992
  let changed = false
953
993
  const matches = value.matches.map((m) => {
954
994
  if (!m || typeof m !== 'object' || typeof m.path !== 'string') return m
955
- const out = posixSeparators(toMsys(m.path))
995
+ const out = toMsys(m.path)
956
996
  if (out === m.path) return m
957
997
  changed = true
958
998
  return { ...m, path: out }
@@ -1858,90 +1898,121 @@ export async function apply(ctx, config = {}) {
1858
1898
  // ── MSYS path translation on every tool dispatch (Windows only) ─────────
1859
1899
  // Covers every preset and mode: the wrapper sits on the global
1860
1900
  // tools/execute waterfall, through which model-direct calls, PTC run_code
1861
- // sub-dispatches, and dynamic-tool calls all pass.
1901
+ // sub-dispatches, and dynamic-tool calls all pass. It carries BOTH dialect
1902
+ // faces of one call — the path-bearing ARGUMENTS on the way in, and the
1903
+ // path-bearing RESULT metadata on the way out.
1904
+ //
1905
+ // The result face rides HERE, not tools/post-execute (v0.23.0, issue #2).
1906
+ // The registry rejects a post-execute decision that replaces `value` while
1907
+ // any listener on that waterfall replaced `content` —
1908
+ // "tools/post-execute accept decision cannot replace both value and content",
1909
+ // raised after the waterfall settles and outside every listener try/catch,
1910
+ // so the call ended as an isError and the model lost the result. An
1911
+ // around-dispatch wrapper may author the result instead: the registry re-runs
1912
+ // the owning output contract on what we return (normalizeDispatchResult ->
1913
+ // createSuccessResult -> render), so the content the model reads is rendered
1914
+ // FROM the dialect value handed back, and this plugin never projects a
1915
+ // `value` on a decision at all — the collision cannot happen. An unchanged
1916
+ // value returns the ORIGINAL result object, so the registry re-renders
1917
+ // nothing. Only a SUCCESSFUL result is touched; failures keep their value
1918
+ // (and their dialect rides the post-execute listener below).
1862
1919
  if (process.platform === 'win32') {
1863
1920
  try {
1864
1921
  ctx.on('tools/execute', (exec, next) => {
1922
+ let echo = false
1923
+ let echoEnv = null
1865
1924
  try {
1866
1925
  const dialect = readDialectSettings(ctx)
1867
- if (exec && exec.arguments && typeof exec.arguments === 'object' && dialect.posixPaths) {
1926
+ if (dialect.posixPaths) {
1927
+ echo = true
1868
1928
  const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
1869
- let translated = translatePathArguments(exec.arguments, env)
1870
- if (exec.name === 'glob' && dialect.globSplit) {
1871
- const globTranslated = translateGlobArguments(translated, env)
1872
- if (globTranslated !== translated) translated = globTranslated
1873
- }
1874
- // v0.20.0: a run_code program is DATA for a native Node process, so
1875
- // a '/c/...' literal inside it never reached this layer and landed
1876
- // on the current drive as '<drive>:\\c\\...'. The same mount table
1877
- // now covers the program's path literals (see rewriteCodePaths;
1878
- // anything unclear leaves the code untouched).
1879
- if (exec.name === 'run_code' && dialect.codePaths && typeof translated.code === 'string') {
1880
- const rewritten = rewriteCodePaths(translated.code, env)
1881
- const next = programPrelude(env) + rewritten
1882
- if (next !== translated.code) translated = { ...translated, code: next }
1929
+ echoEnv = env
1930
+ if (exec && exec.arguments && typeof exec.arguments === 'object') {
1931
+ let translated = translatePathArguments(exec.arguments, env)
1932
+ if (exec.name === 'glob' && dialect.globSplit) {
1933
+ const globTranslated = translateGlobArguments(translated, env)
1934
+ if (globTranslated !== translated) translated = globTranslated
1935
+ }
1936
+ // v0.20.0: a run_code program is DATA for a native Node process, so
1937
+ // a '/c/...' literal inside it never reached this layer and landed
1938
+ // on the current drive as '<drive>:\\c\\...'. The same mount table
1939
+ // now covers the program path literals (see rewriteCodePaths;
1940
+ // anything unclear leaves the code untouched).
1941
+ if (exec.name === 'run_code' && dialect.codePaths && typeof translated.code === 'string') {
1942
+ const rewritten = rewriteCodePaths(translated.code, env)
1943
+ const nextCode = programPrelude(env) + rewritten
1944
+ if (nextCode !== translated.code) translated = { ...translated, code: nextCode }
1945
+ }
1946
+ if (translated !== exec.arguments) exec.arguments = translated
1883
1947
  }
1884
- if (translated !== exec.arguments) exec.arguments = translated
1885
1948
  }
1886
1949
  } catch { /* never block a call on translation */ }
1887
- return next()
1950
+ if (!echo) return next()
1951
+ return next().then((result) => {
1952
+ try {
1953
+ if (!result || typeof result !== 'object' || result.isError !== false) return result
1954
+ if (!result.value || typeof result.value !== 'object') return result
1955
+ const value = rewriteResultPaths(exec && exec.name, result.value, echoEnv)
1956
+ return value === result.value ? result : { ...result, value }
1957
+ } catch { return result }
1958
+ })
1888
1959
  })
1889
- console.log(TAG + ' MSYS drive-root translation active on tool dispatch')
1960
+ console.log(TAG + ' MSYS path translation active on tool dispatch (arguments and result metadata)')
1890
1961
  } catch (error) {
1891
1962
  console.log(TAG + ' tools/execute wiring failed: ' + (error?.message ?? error))
1892
1963
  }
1893
1964
  }
1894
1965
 
1895
- // ── result-path dialect on tools/post-execute (Windows only, gated; v0.10.4) ──
1896
- // Successful results carry Windows-form path METADATA (read.path, glob
1897
- // paths[], grep matches[].path) out of the Node fs layer. The official
1898
- // post-execute waterfall allows replacing the value projection, so while
1899
- // posixPaths is on those metadata fields flow back in the MSYS dialect and
1900
- // the model never sees a Windows path echoed by a successful call. File
1901
- // content and error results are untouched (see rewriteResultPaths).
1966
+ // ── failure dialect on tools/post-execute (Windows only, gated; v0.17.1) ─
1967
+ // A FAILURE has two faces: `content` (the text the model reads on the failed
1968
+ // call, and what the UI card shows) and `error.message` (what the PTC bridge
1969
+ // hands a running program as its caught ToolCallError, while the durable
1970
+ // record keeps the structured identity). Rewriting only the content left a
1971
+ // program that printed a caught error holding the Windows form while every
1972
+ // other face said MSYS (v0.22.0).
1973
+ //
1974
+ // This listener projects CONTENT ONLY, and that is deliberate (v0.23.0,
1975
+ // issue #2): a decision carrying both `content` and `value` makes the
1976
+ // registry throw, and a replacement value is illegal on a failed result
1977
+ // anyway. The SUCCESS face therefore lives on the tools/execute wrapper
1978
+ // above, and this listener can never collide with any other plugin.
1979
+ //
1980
+ // error.message is rewritten IN PLACE on the result the waterfall was handed:
1981
+ // the registry keeps `result.error` by reference until the final
1982
+ // materialization, so this write reaches the PTC bridge and the record.
1983
+ // A post-execute `block` decision would rebuild the error as a bare message
1984
+ // and drop its structured identity — never do that.
1902
1985
  if (process.platform === 'win32') {
1903
1986
  try {
1904
1987
  ctx.on('tools/post-execute', (exec, result, next) => {
1905
- let patch
1906
1988
  let contentPatch
1907
1989
  try {
1908
1990
  const dialect = readDialectSettings(ctx)
1909
- if (dialect.posixPaths && result && typeof result === 'object') {
1910
- if (result.isError === false && result.value && typeof result.value === 'object') {
1911
- const value = rewriteResultPaths(exec && exec.name, result.value, dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null)
1912
- if (value !== result.value) patch = value
1913
- } else if (result.isError === true && dialect.errorDialect && exec && Array.isArray(result.content)) {
1914
- // v0.22.0: BOTH faces of a failure carry the dialect — the
1915
- // content the model reads on a failed call, and the result's own
1916
- // error.message, which a PTC program receives as its caught
1917
- // ToolCallError message and the durable record keeps.
1918
- const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
1919
- if (exec.name === 'run_code') {
1920
- // paths only — the /dev/null hint is for file tools
1921
- const content = rewriteErrorContent(result.content, { nulHint: false, env })
1922
- if (content !== result.content) contentPatch = content
1923
- rewriteFailureMessage(result, env)
1924
- } else if (ERROR_CONTENT_TOOLS.has(exec.name)) {
1925
- const content = rewriteErrorContent(result.content, { env })
1926
- if (content !== result.content) contentPatch = content
1927
- rewriteFailureMessage(result, env)
1928
- }
1991
+ if (dialect.posixPaths && dialect.errorDialect && exec && result && typeof result === 'object'
1992
+ && result.isError === true && Array.isArray(result.content)) {
1993
+ const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
1994
+ if (exec.name === 'run_code') {
1995
+ // paths only — the /dev/null hint is for file tools
1996
+ const content = rewriteErrorContent(result.content, { nulHint: false, env })
1997
+ if (content !== result.content) contentPatch = content
1998
+ rewriteFailureMessage(result, env)
1999
+ } else if (ERROR_CONTENT_TOOLS.has(exec.name)) {
2000
+ const content = rewriteErrorContent(result.content, { env })
2001
+ if (content !== result.content) contentPatch = content
2002
+ rewriteFailureMessage(result, env)
1929
2003
  }
1930
2004
  }
1931
2005
  } catch { /* never block a result */ }
1932
2006
  const chain = next()
1933
- if (patch === undefined && contentPatch === undefined) return chain
2007
+ if (contentPatch === undefined) return chain
1934
2008
  return chain.then((decision) => {
1935
2009
  try {
1936
- if (decision && decision.kind === 'accept') {
1937
- if (patch !== undefined) return { ...decision, value: patch }
1938
- return { ...decision, content: contentPatch }
1939
- }
2010
+ if (decision && decision.kind === 'accept') return { ...decision, content: contentPatch }
1940
2011
  } catch { /* keep the downstream decision */ }
1941
2012
  return decision
1942
2013
  })
1943
2014
  })
1944
- console.log(TAG + ' result-path dialect active on tools/post-execute')
2015
+ console.log(TAG + ' failure dialect active on tools/post-execute (content + error.message)')
1945
2016
  } catch (error) {
1946
2017
  console.log(TAG + ' tools/post-execute wiring failed: ' + (error?.message ?? error))
1947
2018
  }
@@ -2068,4 +2139,4 @@ export async function apply(ctx, config = {}) {
2068
2139
  }
2069
2140
 
2070
2141
  // Test surface: pure helpers, no Cordis context required.
2071
- export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, 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 }
2142
+ 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 }