dsh-gitbash-shell 0.23.0 → 0.24.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/cordis.patch.yml CHANGED
@@ -34,7 +34,10 @@
34
34
  timeoutMs: 60000
35
35
  bashPath: "C:/Program Files/Git/bin/bash.exe"
36
36
 
37
- - id: gitbash-presets
37
+ # Row id 'gitbash-shell' (was 'gitbash-presets' through v0.23.0): dsh >= 0.1.7
38
+ # keys a plugin's live Config form and the one-shot legacy settings.yaml
39
+ # import by the row id, so it now matches the settings namespace string.
40
+ - id: gitbash-shell
38
41
  name: dsh-gitbash-shell
39
42
 
40
43
  # Edit the dsh-better-sidebar row when it is mounted (their own bundle
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.23.0",
3
+ "version": "0.24.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/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none">
2
+ <rect x="4" y="6" width="28" height="24" rx="3.5" stroke="#4493f8" stroke-width="1.8"/>
3
+ <path d="M4 12h28" stroke="#4493f8" stroke-width="1.4"/>
4
+ <circle cx="7.6" cy="9" r="1" fill="#4493f8"/>
5
+ <circle cx="11" cy="9" r="1" fill="#4493f8"/>
6
+ <path d="M10 18.5l4 3.5-4 3.5M17 25.5h8" stroke="#f0883e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
7
+ </svg>
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Git Bash Shell",
4
+ "description": "Swaps Windows' PowerShell shell for Git for Windows bash: a bash-sandbox-compatible executor, a unified MSYS path dialect across every tool (posixPaths and friends, live-editable), and the four * Git Bash agent presets declared straight onto the roster on dsh 0.1.7+."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Git Bash Shell",
4
+ "description": "把 Windows 的 PowerShell shell 换成 Git for Windows bash:兼容 bash-sandbox 的执行器、全工具统一的 MSYS 路径方言(posixPaths 等开关可实时编辑),以及 dsh 0.1.7+ 上直接声明进取的四个 * · Git Bash 预设。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-gitbash-shell",
3
- "version": "0.23.0",
3
+ "version": "0.24.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": {
@@ -11,7 +11,8 @@
11
11
  "exports": {
12
12
  ".": "./src/index.js",
13
13
  "./shell": "./src/shell.js",
14
- "./client": "./src/client.js"
14
+ "./client": "./src/client.js",
15
+ "./locale/*.json": "./locale/*.json"
15
16
  },
16
17
  "engines": {
17
18
  "node": ">=18",
@@ -38,7 +39,9 @@
38
39
  "dsh.plugin.json",
39
40
  "README.md",
40
41
  "README_EN.md",
41
- "LICENSE"
42
+ "LICENSE",
43
+ "locale",
44
+ "icon.svg"
42
45
  ],
43
46
  "scripts": {
44
47
  "test": "node --test tests/smoke.mjs",
@@ -63,5 +66,9 @@
63
66
  "homepage": "https://github.com/KannaKuron/dsh-gitbash-shell",
64
67
  "bugs": {
65
68
  "url": "https://github.com/KannaKuron/dsh-gitbash-shell/issues"
69
+ },
70
+ "icon": "./icon.svg",
71
+ "devDependencies": {
72
+ "@deepseek-ai/schemastery": "^3.18.1"
66
73
  }
67
74
  }
package/src/client.js CHANGED
@@ -1058,11 +1058,46 @@ window.__ModuleLoader__.load({
1058
1058
 
1059
1059
  exports.name = "dsh-gitbash-shell/client";
1060
1060
 
1061
- /** Required client services: locale runtime, settings scopes, slots. */
1062
- exports.inject = ["locale", "settingsScope", "slots"];
1061
+ /**
1062
+ * Required client services: only era-guaranteed ones are hard-injected;
1063
+ * the settings face is acquired OPTIONALLY (dsh 0.1.7 removed the
1064
+ * settingsScope service and a hard inject would leave this fiber PENDING
1065
+ * forever, taking the card down with it).
1066
+ */
1067
+ exports.inject = ["locale", "slots"];
1063
1068
 
1064
1069
  exports.apply = function (ctx) {
1065
- var scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
1070
+ // Era-split settings face (same contract both eras: getSnapshot/set/unset).
1071
+ var scope = null;
1072
+
1073
+ // OLD era (dsh <= 0.1.6): bound settings scope.
1074
+ try {
1075
+ ctx.inject(["settingsScope"], function (sctx) {
1076
+ try {
1077
+ var svc = sctx && sctx.settingsScope;
1078
+ if (svc && typeof svc.bind === "function") scope = svc.bind({ namespace: SETTINGS_NAMESPACE });
1079
+ } catch (error) {
1080
+ console.warn(TAG + " settingsScope acquisition failed:", error && error.message ? error.message : error);
1081
+ }
1082
+ });
1083
+ } catch (error) {
1084
+ console.warn(TAG + " settingsScope wiring failed:", error && error.message ? error.message : error);
1085
+ }
1086
+
1087
+ // NEW era (dsh >= 0.1.7): one ConfigForm per live profile entry; the form
1088
+ // key is the row id "gitbash-shell" (same string as the old namespace).
1089
+ try {
1090
+ ctx.inject(["configForms"], function (fctx) {
1091
+ try {
1092
+ var forms = fctx && fctx.configForms;
1093
+ if (forms && typeof forms.get === "function") scope = forms.get(SETTINGS_NAMESPACE);
1094
+ } catch (error) {
1095
+ console.warn(TAG + " configForms acquisition failed:", error && error.message ? error.message : error);
1096
+ }
1097
+ });
1098
+ } catch (error) {
1099
+ console.warn(TAG + " configForms wiring failed:", error && error.message ? error.message : error);
1100
+ }
1066
1101
  /* The card's only copy entry point: a live lookup, never a captured
1067
1102
  dictionary — the language preference switches without a reload. */
1068
1103
  var t = translatorOf(ctx);
@@ -0,0 +1,296 @@
1
+ /**
2
+ * dsh-gitbash-shell — declarative composition rows (dsh >= 0.1.7).
3
+ *
4
+ * dsh 0.1.7 removed directory presets; a preset is a definition registered
5
+ * through agentPresets.register(). This module is the committed, reviewable
6
+ * composition DATA for the four Git Bash variants — the new-era counterpart
7
+ * of the committed assets/<variant>/agent.cordis*.yml texts the materializer
8
+ * writes on older hosts.
9
+ *
10
+ * Every variant mirrors the official 0.1.7 preset it replaces (packages/
11
+ * bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
12
+ * one Git Bash delta: the bash rows are always on and the pwsh rows are
13
+ * always off (the host executor is this plugin's shell.js — on non-Windows
14
+ * hosts that is the native stack anyway, which is exactly what the old
15
+ * materialized variants encoded).
16
+ */
17
+
18
+ /** Default Git Bash binary — must match src/shell.js and the patch config. */
19
+ const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
20
+
21
+ function off(value) {
22
+ return value === true ? { disabled: true } : {}
23
+ }
24
+
25
+ const PLAN_SECTION = [
26
+ 'You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user\'s conversational agreement — including an answer confirming you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.',
27
+ '',
28
+ 'Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.',
29
+ '',
30
+ 'The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs to exit_plan_mode.',
31
+ '',
32
+ 'Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.',
33
+ '',
34
+ 'Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.',
35
+ '',
36
+ 'When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch the session mode manually; do not proceed with implementation.',
37
+ ].join('\n')
38
+
39
+ /**
40
+ * The shared full-tool rows for standard / ptc / cordis variants (pure).
41
+ * @param {object} input
42
+ * @param {'standard'|'ptc'|'cordis'} input.kind - which official preset this
43
+ * variant mirrors (workflow rows follow it: enabled on standard/cordis,
44
+ * disabled on ptc).
45
+ * @param {boolean} input.gitBash - bash rows always on, pwsh rows always off.
46
+ * @param {string|undefined} input.skillsDir - Creation authoring skills
47
+ * directory (cordis variant only; resolved beside the agent-preset package).
48
+ * @returns {object[]} the declarative plugins list.
49
+ */
50
+ export function pluginsFor({ kind, gitBash, skillsDir }) {
51
+ const win = typeof process !== 'undefined' && process.platform === 'win32'
52
+ const bashDisabled = gitBash ? false : win
53
+ const pwshDisabled = gitBash ? true : !win
54
+ const workflowOn = kind !== 'ptc'
55
+ const rows = [
56
+ {
57
+ id: 'persona',
58
+ name: '@deepseek-ai/dsh-persona',
59
+ config: {
60
+ prefix: 'You are a coding agent powered by the {{model}} model.',
61
+ suffix: 'Your working directory is {{cwd}}.',
62
+ },
63
+ },
64
+ {
65
+ id: 'agent-instructions',
66
+ name: '@deepseek-ai/dsh-agent-instructions',
67
+ config: { maxBytes: 65536 },
68
+ },
69
+ { id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', ...off(bashDisabled) },
70
+ { id: 'tool-pwsh', name: '@deepseek-ai/dsh-tool-pwsh', ...off(pwshDisabled) },
71
+ { id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
72
+ {
73
+ id: 'tool-fs-search',
74
+ name: '@deepseek-ai/dsh-tool-fs-search',
75
+ config: { sampleOverCapGlobResults: false },
76
+ },
77
+ { id: 'tool-jobs', name: '@deepseek-ai/dsh-tool-jobs' },
78
+ {
79
+ id: 'skill-filesystem',
80
+ name: '@deepseek-ai/dsh-skill-filesystem',
81
+ config: kind === 'cordis' && skillsDir ? { customSkillDirs: [skillsDir] } : {},
82
+ },
83
+ { id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
84
+ { id: 'command-goal', name: '@deepseek-ai/dsh-command-goal' },
85
+ { id: 'tool-goal', name: '@deepseek-ai/dsh-tool-goal' },
86
+ {
87
+ id: 'planning',
88
+ name: 'cordis:group',
89
+ group: true,
90
+ isolate: { planMode: true },
91
+ config: [
92
+ {
93
+ id: 'plan-mode',
94
+ name: '@deepseek-ai/dsh-plan-mode',
95
+ config: { section: PLAN_SECTION },
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ id: 'compaction',
101
+ name: 'cordis:group',
102
+ group: true,
103
+ isolate: { compaction: true, toolResultPruner: true },
104
+ config: [
105
+ { id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
106
+ { id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
107
+ {
108
+ id: 'tool-result-pruner',
109
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner',
110
+ config: { thresholdChars: 8192, headChars: 4096, tailChars: 1024 },
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ id: 'delegation',
116
+ name: 'cordis:group',
117
+ group: true,
118
+ isolate: { workflowEngine: true },
119
+ config: [
120
+ { id: 'tool-subagent-control', name: '@deepseek-ai/dsh-tool-subagent-control' },
121
+ { id: 'tool-subagent-list-agents', name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' },
122
+ {
123
+ id: 'tool-subagent',
124
+ name: '@deepseek-ai/dsh-tool-subagent',
125
+ config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true, backgroundMode: 'continuable' },
126
+ },
127
+ {
128
+ id: 'tool-subagent-fork',
129
+ name: '@deepseek-ai/dsh-tool-subagent',
130
+ config: { provider: 'fork', toolName: 'subagent_fork', backgroundMode: 'continuable' },
131
+ },
132
+ {
133
+ id: 'tool-subagent-codex',
134
+ name: '@deepseek-ai/dsh-tool-subagent',
135
+ disabled: true,
136
+ config: { provider: 'codex', toolName: 'subagent_codex', backgroundMode: 'one-shot', maxDepth: 'provider-managed' },
137
+ },
138
+ {
139
+ id: 'tool-subagent-claude-code',
140
+ name: '@deepseek-ai/dsh-tool-subagent',
141
+ disabled: true,
142
+ config: { provider: 'claude-code', toolName: 'subagent_claude_code', backgroundMode: 'one-shot', maxDepth: 'provider-managed' },
143
+ },
144
+ {
145
+ id: 'workflow-ptc',
146
+ name: '@deepseek-ai/dsh-workflow-ptc',
147
+ ...off(!workflowOn),
148
+ config: { provider: 'spawn' },
149
+ },
150
+ { id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow', ...off(!workflowOn) },
151
+ {
152
+ id: 'tool-ralph',
153
+ name: '@deepseek-ai/dsh-tool-ralph',
154
+ disabled: true,
155
+ config: { subagentProvider: 'spawn', maxRounds: 64 },
156
+ },
157
+ ],
158
+ },
159
+ { id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user' },
160
+ {
161
+ id: 'tool-todo',
162
+ name: '@deepseek-ai/dsh-tool-todo',
163
+ config: { allowParallelInProgress: true },
164
+ },
165
+ {
166
+ id: 'tool-web',
167
+ name: '@deepseek-ai/dsh-tool-web',
168
+ config: { fetch: true, searchTimeoutMs: 60000 },
169
+ },
170
+ ]
171
+ if (kind === 'ptc') {
172
+ rows.push({
173
+ id: 'tool-presentation',
174
+ name: '@deepseek-ai/dsh-agent-tool-presentation',
175
+ config: { mode: 'ptc' },
176
+ })
177
+ }
178
+ if (kind === 'cordis') {
179
+ rows.push({ id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' })
180
+ }
181
+ rows.push({ id: 'present', name: '@deepseek-ai/dsh-tool-present' })
182
+ rows.push({
183
+ id: 'tool-plugin-manager',
184
+ name: '@deepseek-ai/dsh-plugin-manager/tools',
185
+ // The official 0.1.7 rows: ptc/standard ship it disabled, cordis keeps it
186
+ // on wherever a profile context exists (every web deployment does).
187
+ ...off(kind !== 'cordis'),
188
+ })
189
+ return rows
190
+ }
191
+
192
+ /**
193
+ * The minimal variant: the official single-tool preset with the Git Bash
194
+ * delta — bash rows always on (the terminal row pins the Git Bash binary on
195
+ * Windows), pwsh rows always off (pure).
196
+ * @returns {object[]} the declarative plugins list.
197
+ */
198
+ export function minimalPluginsFor() {
199
+ const win = typeof process !== 'undefined' && process.platform === 'win32'
200
+ return [
201
+ {
202
+ id: 'persona',
203
+ name: '@deepseek-ai/dsh-persona',
204
+ config: {
205
+ prefix: 'You are a helpful software engineer assistant.',
206
+ complete: true,
207
+ includeRuntimeContext: false,
208
+ },
209
+ },
210
+ {
211
+ id: 'persistent-shell',
212
+ name: 'cordis:group',
213
+ group: true,
214
+ isolate: { terminals: true },
215
+ config: [
216
+ { id: 'pty', name: '@deepseek-ai/dsh-terminal' },
217
+ {
218
+ id: 'terminal-bash',
219
+ name: '@deepseek-ai/dsh-terminal-bash',
220
+ disabled: false,
221
+ config: {
222
+ timeoutMs: 300000,
223
+ shellPath: win ? DEFAULT_GIT_BASH : '/bin/bash',
224
+ },
225
+ },
226
+ {
227
+ id: 'persistent-bash',
228
+ name: '@deepseek-ai/dsh-tool-bash-persistent',
229
+ disabled: false,
230
+ config: {
231
+ timeoutMs: 300000,
232
+ description: [
233
+ 'Run commands in a bash shell (Git for Windows bash on Windows: native paths like C:/... work, and PATH plus the rest of the environment are inherited from the host)',
234
+ '* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
235
+ '* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.',
236
+ '* State is persistent across command calls and discussions with the user.',
237
+ '* To inspect a particular line range of a file, e.g. lines 10-25, try \'sed -n 10,25p /path/to/the/file\'.',
238
+ '* Please avoid commands that may produce a very large amount of output.',
239
+ '* Please run long lived commands in the background, e.g. \'sleep 10 &\' or start a server in the background.',
240
+ ].join('\n'),
241
+ },
242
+ },
243
+ {
244
+ id: 'terminal-pwsh',
245
+ name: '@deepseek-ai/dsh-terminal-bash',
246
+ disabled: true,
247
+ config: { shellDialect: 'pwsh', timeoutMs: 300000 },
248
+ },
249
+ {
250
+ id: 'persistent-pwsh',
251
+ name: '@deepseek-ai/dsh-tool-pwsh-persistent',
252
+ disabled: true,
253
+ config: {
254
+ timeoutMs: 300000,
255
+ description: [
256
+ 'Run commands in a PowerShell shell',
257
+ '* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
258
+ "* You don't have access to the internet via this tool.",
259
+ '* Use native Windows paths (C:/...) and $env:NAME variables; this is PowerShell, not bash.',
260
+ '* Please avoid commands that may produce a very large amount of output.',
261
+ '* Please run long lived commands in the background, e.g. \'Start-Job\' or start a server with Start-Process.',
262
+ ].join('\n'),
263
+ },
264
+ },
265
+ ],
266
+ },
267
+ ]
268
+ }
269
+
270
+ /** Display metadata for the four registrations (mirrors the committed preset.yml files). */
271
+ export const PRESET_META = {
272
+ 'standard-gitbash': {
273
+ kind: 'standard',
274
+ name: '标准模式 · Git Bash',
275
+ description: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。(Shell 使用 Git Bash)',
276
+ order: 1,
277
+ },
278
+ 'code-gitbash': {
279
+ kind: 'ptc',
280
+ name: 'PTC 模式 · Git Bash',
281
+ description: '功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。(Shell 使用 Git Bash)',
282
+ order: 2,
283
+ },
284
+ 'minimal-gitbash': {
285
+ kind: 'minimal',
286
+ name: '极简模式 · Git Bash',
287
+ description: '仅提供持久 shell 的单工具编码 Agent。(Shell 使用 Git Bash)',
288
+ order: 3,
289
+ },
290
+ 'cordis-gitbash': {
291
+ kind: 'cordis',
292
+ name: '创造模式 · Git Bash',
293
+ description: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。(Shell 使用 Git Bash)',
294
+ order: 4,
295
+ },
296
+ }
package/src/index.js CHANGED
@@ -52,8 +52,11 @@ 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'
55
56
  import { fileURLToPath } from 'node:url'
56
57
 
58
+ import { PRESET_META, minimalPluginsFor, pluginsFor } from './compositions.js'
59
+
57
60
  /** Plugin identity for cordis.yml rows. */
58
61
  export const name = 'dsh-gitbash-shell'
59
62
 
@@ -662,6 +665,79 @@ export function installRegisterShim(reg) {
662
665
 
663
666
  const SETTINGS_NAMESPACE = 'gitbash-shell'
664
667
 
668
+ /**
669
+ * dsh >= 0.1.7 marks a Config field live-editable without remount; a
670
+ * 0.1.6-era schemastery predates the method and the guard keeps this module
671
+ * loadable there (values then behave as ordinary config, read through the
672
+ * registered settings namespace instead).
673
+ */
674
+ function live(schema) {
675
+ return typeof schema.volatile === 'function' ? schema.volatile() : schema
676
+ }
677
+
678
+ /**
679
+ * Row Config = the settings surface on dsh >= 0.1.7 (values persist under the
680
+ * row id; the patch row id is 'gitbash-shell', the same string as the old
681
+ * settings namespace, so the one-shot legacy settings.yaml import maps old
682
+ * user values onto the new home). Inert metadata on older hosts.
683
+ */
684
+ export const Config = Schema.object({
685
+ // Row-level knob predating the Config surface (invariant 6): which preset
686
+ // variants to serve. Ordinary config — an edit remounts the plugin, which
687
+ // is the right weight for a list that changes the roster.
688
+ presets: Schema.array(Schema.string()).default(PRESET_IDS),
689
+ posixPaths: live(Schema.boolean().default(true)),
690
+ virtualMounts: live(Schema.boolean().default(true)),
691
+ globSplit: live(Schema.boolean().default(true)),
692
+ errorDialect: live(Schema.boolean().default(true)),
693
+ codePaths: live(Schema.boolean().default(true)),
694
+ gitAutocrlf: live(Schema.boolean().default(true)),
695
+ bashPath: live(Schema.string().default('')),
696
+ adoptSidebar: live(Schema.boolean().default(true)),
697
+ })
698
+
699
+ /** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
700
+ export function valueOf(value) {
701
+ return value && typeof value.get === 'function' ? value.get() : value
702
+ }
703
+
704
+ /**
705
+ * Era-aware live settings reader for everything wired inside apply(): the
706
+ * NEW era reads this plugin's own Config refs (no listener needed — every
707
+ * consumer here already re-reads per dispatch/assembly), the OLD era keeps
708
+ * reading the registered namespace through the settings service.
709
+ * @param {object} ctx - the plugin's mounting context.
710
+ * @param {object|undefined} config - apply()'s resolved Config.
711
+ * @returns {{ dialect: () => object, posix: () => boolean, adoptSidebar: () => boolean }}
712
+ */
713
+ function makeLiveReader(ctx, config) {
714
+ let legacy = false
715
+ try {
716
+ const settings = ctx.get('settings')
717
+ legacy = !!(settings && typeof settings.register === 'function')
718
+ } catch { legacy = false }
719
+ const hasConfig = !!(config && typeof config === 'object')
720
+ const dialect = () => {
721
+ if (legacy || !hasConfig) return readDialectSettings(ctx)
722
+ const bash = valueOf(config.bashPath)
723
+ return {
724
+ posixPaths: valueOf(config.posixPaths) === true,
725
+ virtualMounts: valueOf(config.virtualMounts) === true,
726
+ globSplit: valueOf(config.globSplit) === true,
727
+ errorDialect: valueOf(config.errorDialect) === true,
728
+ codePaths: valueOf(config.codePaths) === true,
729
+ gitAutocrlf: valueOf(config.gitAutocrlf) === true,
730
+ bashPath: typeof bash === 'string' ? bash : '',
731
+ adoptSidebar: valueOf(config.adoptSidebar) !== false,
732
+ }
733
+ }
734
+ return {
735
+ dialect,
736
+ posix: () => dialect().posixPaths === true,
737
+ adoptSidebar: () => dialect().adoptSidebar !== false,
738
+ }
739
+ }
740
+
665
741
  /** The full directive text, injected only while posixPaths is on. */
666
742
  const POSIX_DIRECTIVE_TEXT = 'The working shell is Git for Windows bash: paths use MSYS drive roots (/c/Users/...), and every tool accepts that form directly — including the bash-native habits (~ home shorthand, /tmp, /dev/null, /usr/bin/...), which resolve in every tool exactly as bash itself resolves them.'
667
743
 
@@ -1630,7 +1706,7 @@ function adoptSidebarShell(ctx, bashPath) {
1630
1706
 
1631
1707
  const run = () => {
1632
1708
  tried += 1
1633
- const ok = reconcileSidebar(readAdoptSidebar(ctx))
1709
+ const ok = reconcileSidebar(liveSettings.adoptSidebar())
1634
1710
  if (ok) {
1635
1711
  // The namespace scope may register after the settings service; hook
1636
1712
  // the live watch once it exists (idempotent via the effect disposer).
@@ -1722,6 +1798,105 @@ function purgeOrphans(userRoot, keep) {
1722
1798
  }
1723
1799
  }
1724
1800
 
1801
+ // ── declarative registration (dsh >= 0.1.7) ──────────────────────────────────
1802
+ //
1803
+ // dsh 0.1.7 removed directory presets: nothing reads ~/.dsh/.agent-presets
1804
+ // any more, and a preset is a definition registered through
1805
+ // agentPresets.register(). The era probe is the register method itself; the
1806
+ // composition data lives in src/compositions.js (the new-era counterpart of
1807
+ // the committed assets/<variant>/ texts).
1808
+
1809
+ /** Resolve the Creation-skills dir beside @deepseek-ai/dsh-agent-preset. */
1810
+ async function resolveSkillsDir() {
1811
+ try {
1812
+ const { createRequire } = await import('node:module')
1813
+ const here = createRequire(import.meta.url)
1814
+ const pkg = here.resolve('@deepseek-ai/dsh-agent-preset/package.json')
1815
+ return join(dirname(pkg), 'skills')
1816
+ } catch {
1817
+ return undefined
1818
+ }
1819
+ }
1820
+
1821
+ /** The legacy preset root, best effort (roster roots are gone on 0.1.7). */
1822
+ function legacyPresetRoot() {
1823
+ try {
1824
+ const env = process.env.DSH_HOME
1825
+ if (env && env.trim() !== '') return join(env, '.agent-presets')
1826
+ return join(homedir(), '.dsh', '.agent-presets')
1827
+ } catch {
1828
+ return undefined
1829
+ }
1830
+ }
1831
+
1832
+ /**
1833
+ * Remove the stale materialized trees a previous (pre-0.1.7-host) version
1834
+ * left behind, when they are still byte-identical to what we wrote (the
1835
+ * marker judges). User-modified and foreign trees stay untouched — the same
1836
+ * ownership rules the materializer itself follows.
1837
+ */
1838
+ function cleanupLegacyTrees(presetIds) {
1839
+ const root = legacyPresetRoot()
1840
+ if (!root) return
1841
+ for (const presetId of presetIds) {
1842
+ const target = join(root, presetId)
1843
+ const state = classify(target)
1844
+ if (state === 'unmodified') {
1845
+ try {
1846
+ rmSync(target, { recursive: true, force: true })
1847
+ console.log(TAG + ' removed the stale materialized preset at ' + target + ' (directory presets are no longer read on this host; the declarative registration replaces it)')
1848
+ } catch (error) {
1849
+ console.log(TAG + ' stale preset cleanup failed: ' + (error && error.message ? error.message : error))
1850
+ }
1851
+ } else if (state === 'user-modified') {
1852
+ console.log(TAG + ' a user-modified preset tree remains at ' + target + ' — left untouched (this host ignores it; delete it manually if unwanted)')
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ /**
1858
+ * Register one variant definition; returns the unregister function. Mount
1859
+ * failures stay visible through the roster's broken diagnostic instead of
1860
+ * failing the boot.
1861
+ */
1862
+ async function registerVariant(ctx, presetId, { gitBashActive, skillsDir }) {
1863
+ const meta = PRESET_META[presetId]
1864
+ if (!meta) return undefined
1865
+ const plugins = meta.kind === 'minimal'
1866
+ ? minimalPluginsFor()
1867
+ : pluginsFor({ kind: meta.kind, gitBash: gitBashActive, skillsDir })
1868
+ return ctx.agentPresets.register({
1869
+ id: presetId,
1870
+ name: meta.name,
1871
+ description: meta.description,
1872
+ order: meta.order,
1873
+ plugins,
1874
+ })
1875
+ }
1876
+
1877
+ /**
1878
+ * Declarative-era wiring: register every configured variant once at startup
1879
+ * and keep exactly one registration per variant for the plugin's lifetime.
1880
+ * The roster picks the entries up immediately; sessions already pinned to a
1881
+ * variant keep their revision until recomposed.
1882
+ */
1883
+ async function runDeclarativeEra(ctx, presetIds) {
1884
+ cleanupLegacyTrees(presetIds)
1885
+ const gitBashActive = process.platform === 'win32'
1886
+ const skillsDir = await resolveSkillsDir()
1887
+ const unregisters = []
1888
+ for (const presetId of presetIds) {
1889
+ try {
1890
+ const unregister = await registerVariant(ctx, presetId, { gitBashActive, skillsDir })
1891
+ if (unregister) unregisters.push(unregister)
1892
+ } catch (error) {
1893
+ console.log(TAG + " preset '" + presetId + "' registration failed: " + (error && error.message ? error.message : error))
1894
+ }
1895
+ }
1896
+ console.log(TAG + ' registered ' + unregisters.length + ' preset(s) declaratively (' + presetIds.join(', ') + ')')
1897
+ ctx.effect(() => () => { for (const off of unregisters) void off() }, 'dsh-gitbash-shell: declarative preset registrations')
1898
+ }
1899
+
1725
1900
  export async function apply(ctx, config = {}) {
1726
1901
  const presetIds = Array.isArray(config.presets) && config.presets.length > 0 ? config.presets : PRESET_IDS
1727
1902
 
@@ -1757,12 +1932,28 @@ export async function apply(ctx, config = {}) {
1757
1932
  const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
1758
1933
  ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
1759
1934
 
1760
- // ── settings namespace: the posixPaths switch (default ON, v0.10.0) ──────
1935
+ // Era-aware live settings: NEW hosts read this plugin's own Config refs
1936
+ // (volatile, re-read per dispatch/assembly), OLD hosts keep the registered
1937
+ // namespace. Every consumer below already re-reads on every use, so a
1938
+ // flipped knob lands on the next dispatch with zero wiring.
1939
+ const liveSettings = makeLiveReader(ctx, config)
1940
+
1941
+ // ── settings namespace: the posixPaths switch (default ON, v0.1.0) — OLD
1942
+ // hosts only; on dsh >= 0.1.7 the row Config above IS the surface.
1943
+
1761
1944
  // Served on the host so the Plugins tab pairs it with the browser card
1762
1945
  // (the tab dispatches Host-served namespaces ∩ registered cards). Dynamic
1763
1946
  // imports keep the zero-dependency smoke path importable; the schema MUST
1764
1947
  // be a callable schemastery object (dsh-settings calls schema(merged)).
1765
- try {
1948
+ const legacySettings = (() => {
1949
+ try {
1950
+ const settings = ctx.get('settings')
1951
+ return !!(settings && typeof settings.register === 'function')
1952
+ } catch {
1953
+ return false
1954
+ }
1955
+ })()
1956
+ if (legacySettings) try {
1766
1957
  ctx.inject(['settings'], (sctx) => {
1767
1958
  Promise.all([import('@deepseek-ai/dsh-settings'), import('@deepseek-ai/schemastery')])
1768
1959
  .then(([ds, sm]) => {
@@ -1832,7 +2023,7 @@ export async function apply(ctx, config = {}) {
1832
2023
  // own env layer (src/shell.js), not this registry.
1833
2024
  variables: { [PATH_DIALECT_KEY]: { description: PATH_DIALECT_DESCRIPTION } },
1834
2025
  resolve() {
1835
- return readPosixPaths(envCtx) ? { [PATH_DIALECT_KEY]: PATH_DIALECT_VALUE } : {}
2026
+ return liveSettings.posix() ? { [PATH_DIALECT_KEY]: PATH_DIALECT_VALUE } : {}
1836
2027
  },
1837
2028
  })
1838
2029
  envCtx.effect(() => unregister, 'dsh-gitbash-shell: shellEnv path-dialect fact')
@@ -1860,7 +2051,7 @@ export async function apply(ctx, config = {}) {
1860
2051
  try {
1861
2052
  ctx.on('system-prompt/assemble', (assembly, _assembleContext, next) => {
1862
2053
  try {
1863
- if (readPosixPaths(ctx) && assembly && typeof assembly === 'object') {
2054
+ if (liveSettings.posix() && assembly && typeof assembly === 'object') {
1864
2055
  for (const section of Array.isArray(assembly.sections) ? assembly.sections : []) {
1865
2056
  if (section && typeof section.text === 'string') section.text = windowsToMsys(section.text)
1866
2057
  }
@@ -1987,7 +2178,7 @@ export async function apply(ctx, config = {}) {
1987
2178
  ctx.on('tools/post-execute', (exec, result, next) => {
1988
2179
  let contentPatch
1989
2180
  try {
1990
- const dialect = readDialectSettings(ctx)
2181
+ const dialect = liveSettings.dialect()
1991
2182
  if (dialect.posixPaths && dialect.errorDialect && exec && result && typeof result === 'object'
1992
2183
  && result.isError === true && Array.isArray(result.content)) {
1993
2184
  const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
@@ -2036,10 +2227,9 @@ export async function apply(ctx, config = {}) {
2036
2227
  order: 126,
2037
2228
  text: () => {
2038
2229
  try {
2039
- const settings = pctx.get('settings')
2040
- const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
2041
- if (!value || value.posixPaths !== true) return ''
2042
- return value.virtualMounts === false ? POSIX_DIRECTIVE_TEXT_STRICT : POSIX_DIRECTIVE_TEXT
2230
+ const dialect = liveSettings.dialect()
2231
+ if (dialect.posixPaths !== true) return ''
2232
+ return dialect.virtualMounts === false ? POSIX_DIRECTIVE_TEXT_STRICT : POSIX_DIRECTIVE_TEXT
2043
2233
  } catch {
2044
2234
  return ''
2045
2235
  }
@@ -2063,6 +2253,19 @@ export async function apply(ctx, config = {}) {
2063
2253
  adoptSidebarShell(ctx, gitBashCapability.bashPath)
2064
2254
  }
2065
2255
 
2256
+ // ── era split: declarative registration on dsh >= 0.1.7 ──────────────────
2257
+ // The register method IS the era signal. On the new host the whole
2258
+ // materialization path below is dead code (nothing reads the directory any
2259
+ // more), so this branch serves the variants and returns.
2260
+ if (ctx.agentPresets && typeof ctx.agentPresets.register === 'function') {
2261
+ try {
2262
+ await runDeclarativeEra(ctx, presetIds)
2263
+ } catch (error) {
2264
+ console.log(TAG + ' declarative registration failed: ' + (error && error.message ? error.message : error))
2265
+ }
2266
+ return
2267
+ }
2268
+
2066
2269
  const roots = ctx.agentPresets?.roots ?? []
2067
2270
  const userRoot = firstUserRoot(roots)
2068
2271
  if (!userRoot) {