dsh-win-multi-bash 0.3.1 → 0.4.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/README.i18n.yaml +2 -2
- package/README.md +36 -5
- package/README.zh.md +35 -4
- package/cordis.patch.yml +52 -10
- package/install.ps1 +179 -0
- package/lib/bash-git/index.js +28 -12
- package/lib/bash-wsl/index.js +23 -13
- package/lib/client.js +558 -0
- package/lib/index.js +46 -1
- package/lib/tool-bash/types/factory.js +33 -77
- package/lib/tool-bash/types/shell-family.js +193 -0
- package/package.json +13 -1
- package/uninstall.ps1 +50 -0
|
@@ -13,12 +13,16 @@ import { isAbsolute, resolve as resolvePath } from 'node:path';
|
|
|
13
13
|
import { defineTool, TOOL_ABORTED } from '@deepseek-ai/dsh-tools';
|
|
14
14
|
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
15
15
|
import { ESCALATION_TARGETS, approveEscalation, canonicalPath, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox';
|
|
16
|
-
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-shell';
|
|
17
16
|
import { ownExecutor } from "./backend.js";
|
|
18
17
|
import { processJob, processOutcome, processSources } from "./background.js";
|
|
19
18
|
import { parseExitStatus, renderResult } from "./render.js";
|
|
19
|
+
import { SHELL_FAMILY_ROW_SPECIFIER, composeToolDescription, familyRowComposed } from "./shell-family.js";
|
|
20
|
+
/**
|
|
21
|
+
* The dialect-owned facts of this plugin's two shell tools. Everything they
|
|
22
|
+
* share lives in one registered prompt section instead (see
|
|
23
|
+
* ./shell-family.js), so a fact stated here is stated only here.
|
|
24
|
+
*/
|
|
20
25
|
const DIALECT_FACTS = {
|
|
21
|
-
posix: { shell: 'bash', invoke: 'bash -c', paths: 'POSIX paths', env: '$VAR', toolchain: 'the full Unix toolchain' },
|
|
22
26
|
msys: {
|
|
23
27
|
shell: 'Git Bash (MSYS2)',
|
|
24
28
|
invoke: 'bash -c',
|
|
@@ -35,80 +39,26 @@ const DIALECT_FACTS = {
|
|
|
35
39
|
};
|
|
36
40
|
/** The tool-layer keys every shell tool instance carries, before its backend partition. */
|
|
37
41
|
const TOOL_CONFIG = {
|
|
38
|
-
enableRunInBackground: z.boolean().default(true),
|
|
42
|
+
enableRunInBackground: z.boolean().default(true).volatile(),
|
|
39
43
|
};
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*/
|
|
48
|
-
export const SHELL_EXIT_STATUS_SECTION = 'Check the [exit code: N] marker on every bash result; investigate failures before moving on. Chain dependent steps with `&&` or `set -o pipefail`: `;` never stops on failure, and `cmd | tail` returns the status of `tail`, not of `cmd`.';
|
|
49
|
-
/**
|
|
50
|
-
* The model-facing description of one shell tool instance. The POSIX variant
|
|
51
|
-
* keeps the legacy wording byte-for-byte (the ACP/headless tool-schema
|
|
52
|
-
* fixtures pin it); msys/wsl instances mirror the official tool-pwsh
|
|
53
|
-
* skeleton (fresh shell, paths/env, exit codes, sandbox, truncation,
|
|
54
|
-
* background, escalation) with only their dialect's shell/paths/env facts
|
|
55
|
-
* plus at most one short dialect note.
|
|
45
|
+
* The model-facing description of one shell tool instance: only what this
|
|
46
|
+
* dialect owns — its shell and invocation, its path form, its variable syntax,
|
|
47
|
+
* and at most one short dialect note. Everything the family shares (fresh
|
|
48
|
+
* calls, exit markers, sandbox markers, truncation, path safety, background,
|
|
49
|
+
* escalation) is registered once for the composition by the
|
|
50
|
+
* `tool-shell-prompt` row, so no tool description repeats it.
|
|
56
51
|
* @param dialect - the shell dialect the instance runs.
|
|
57
|
-
* @param backgroundEnabled - whether `run_in_background` is advertised.
|
|
58
|
-
* @param escalationModes - the escalation targets this composition advertises;
|
|
59
|
-
* empty adds no same-turn escalation guidance.
|
|
60
52
|
* @returns the tool description passed to `defineTool`.
|
|
61
53
|
*/
|
|
62
|
-
export function shellDescription(dialect
|
|
63
|
-
const background = backgroundEnabled
|
|
64
|
-
? 'Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.'
|
|
65
|
-
: 'Background execution is not available; long-running commands must finish within the timeout.';
|
|
66
|
-
if (dialect === 'posix') {
|
|
67
|
-
const base = 'Execute a bash command (`bash -c`) and return its stdout/stderr. '
|
|
68
|
-
+ 'Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — '
|
|
69
|
-
+ 'pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. '
|
|
70
|
-
+ `Current harness environment facts are exposed through managed \`\$${DSH_ENV_PREFIX}*\` variables; inspect them when needed. `
|
|
71
|
-
+ 'Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. '
|
|
72
|
-
+ 'Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. '
|
|
73
|
-
+ background;
|
|
74
|
-
return base + escalationTail(escalationModes);
|
|
75
|
-
}
|
|
54
|
+
export function shellDescription(dialect) {
|
|
76
55
|
const facts = DIALECT_FACTS[dialect];
|
|
56
|
+
if (facts === undefined)
|
|
57
|
+
throw new Error(`unknown shell dialect "${dialect}"`);
|
|
77
58
|
const note = facts.note === undefined ? '' : ` Note: ${facts.note}.`;
|
|
78
|
-
|
|
79
|
-
+ 'Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — '
|
|
80
|
-
+ 'pass `workdir` instead of using `cd`. '
|
|
59
|
+
return `Execute a ${facts.shell} command (${facts.invoke}) and return its stdout/stderr. `
|
|
81
60
|
+ `Paths use ${facts.paths}; read environment variables with ${facts.env}.`
|
|
82
|
-
+ note
|
|
83
|
-
+ 'Non-zero exits are reported as `[exit code: N]`. '
|
|
84
|
-
+ `Current harness environment facts are exposed through managed \`\$${DSH_ENV_PREFIX}*\` variables; inspect them when needed. `
|
|
85
|
-
+ 'Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. '
|
|
86
|
-
+ 'Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. '
|
|
87
|
-
+ 'Before any delete or move, verify that the resolved absolute target path is the intended one; never run it against a computed path you have not checked. '
|
|
88
|
-
+ 'An unset variable expands to an empty string, so guard variables in such paths with `${VAR:?}`. '
|
|
89
|
-
+ background;
|
|
90
|
-
return base + escalationTail(escalationModes);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* The same-turn escalation guidance appended after a denial marker. Kept in
|
|
94
|
-
* one place because every dialect shares the exact approval contract.
|
|
95
|
-
* @param escalationModes - the escalation targets this composition advertises.
|
|
96
|
-
* @returns the guidance sentence, or '' when no escalation is advertised.
|
|
97
|
-
*/
|
|
98
|
-
function escalationTail(escalationModes) {
|
|
99
|
-
if (escalationModes.length === 0)
|
|
100
|
-
return '';
|
|
101
|
-
return ' Attempting a command the sandbox may deny is safe and expected: run it and read the '
|
|
102
|
-
+ 'marker rather than assuming the denial. When a command is denied and a wider mode would let it '
|
|
103
|
-
+ 'succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry '
|
|
104
|
-
+ 'the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) '
|
|
105
|
-
+ 'plus a one-sentence `justification`. Do not detour through chat to ask permission first — the '
|
|
106
|
-
+ 'approval prompt raised by that retry is how the user consents. If the session states approval '
|
|
107
|
-
+ 'prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. '
|
|
108
|
-
+ 'Never escalate speculatively: ground the request in a real denial — normally the one this command '
|
|
109
|
-
+ 'just hit; escalating up front is fine only when this session already denied the same access. '
|
|
110
|
-
+ 'A rejected escalation is final for that command — stop and explain, never work around '
|
|
111
|
-
+ 'it — but it does not forbid attempting or escalating other commands later.';
|
|
61
|
+
+ note;
|
|
112
62
|
}
|
|
113
63
|
function validateShellArgs(args) {
|
|
114
64
|
if (args.command.trim().length === 0) {
|
|
@@ -254,12 +204,19 @@ export function defineShellTool(def) {
|
|
|
254
204
|
});
|
|
255
205
|
return {
|
|
256
206
|
name: `tool-${def.toolName}`,
|
|
207
|
+
// The tool registers no prompt section of its own: the family's shared
|
|
208
|
+
// guidance is owned by the separate `tool-shell-prompt` row (see
|
|
209
|
+
// ./shell-family.js), so this row needs no `systemPrompt`.
|
|
257
210
|
// The last three are the owned executor's own requirements; injecting
|
|
258
211
|
// them here is what lets the backend be built during apply.
|
|
259
|
-
inject: ['tools', '
|
|
212
|
+
inject: ['tools', 'shellEnv', 'subprocess', 'sandbox', 'sandboxPolicy'],
|
|
260
213
|
Config,
|
|
261
214
|
async apply(ctx, config = {}) {
|
|
262
|
-
|
|
215
|
+
// Volatile: the loader hands a live reference, so a saved value is
|
|
216
|
+
// read here rather than off a snapshot.
|
|
217
|
+
const backgroundEnabled = config.enableRunInBackground?.get() ?? true;
|
|
218
|
+
const familyPresent = familyRowComposed(ctx);
|
|
219
|
+
if (!familyPresent) ctx.logger?.warn?.(`tool-${def.toolName}: the row that owns the shared shell prompt section (${SHELL_FAMILY_ROW_SPECIFIER}) is not composed, so this tool's description carries that guidance itself. Re-enable that row to state it once for the whole family.`);
|
|
263
220
|
const { executor } = ownExecutor(ctx, def.Executor, config[def.configKey]);
|
|
264
221
|
// The confinement probe is async on 0.1.7 (the provider's `confine`
|
|
265
222
|
// returns a promise), so the escalation surface settles before the
|
|
@@ -309,15 +266,14 @@ export function defineShellTool(def) {
|
|
|
309
266
|
signal: exec.signal,
|
|
310
267
|
});
|
|
311
268
|
};
|
|
312
|
-
// Cross-call guidance belongs in the prompt rather than one-call schema prose.
|
|
313
|
-
ctx.systemPrompt.section({
|
|
314
|
-
name: `tool:${def.toolName}`,
|
|
315
|
-
order: 105,
|
|
316
|
-
text: SHELL_EXIT_STATUS_SECTION,
|
|
317
|
-
});
|
|
318
269
|
ctx.tools.register(defineTool({
|
|
319
270
|
name: def.toolName,
|
|
320
|
-
description:
|
|
271
|
+
description: composeToolDescription({
|
|
272
|
+
own: shellDescription(def.dialect),
|
|
273
|
+
familyPresent,
|
|
274
|
+
background: backgroundEnabled,
|
|
275
|
+
escalation: escalationModes.length > 0,
|
|
276
|
+
}),
|
|
321
277
|
parameters: {
|
|
322
278
|
command: { type: 'string', required: true, description: 'The bash command to execute.' },
|
|
323
279
|
description: {
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared half of the shell guidance: what every shell tool call in this
|
|
3
|
+
* composition has in common, stated once for the whole family.
|
|
4
|
+
*
|
|
5
|
+
* Why the split exists (measured on 0.3.1: the two tool descriptions were 1299
|
|
6
|
+
* and 2164 characters, 1117 of them byte-identical, and the same exit-status
|
|
7
|
+
* paragraph was registered twice — once per tool instance):
|
|
8
|
+
* - A tool description is ambient only while that tool is mounted, so it can
|
|
9
|
+
* carry only what belongs to that tool. Anything the family shares belongs
|
|
10
|
+
* in one prompt section; anything dialect-specific (shell name, path form,
|
|
11
|
+
* MSYS note) stays with its own tool.
|
|
12
|
+
* - The section has exactly one owner — the `tool-shell-prompt` row — because
|
|
13
|
+
* prompt sections live in one global layer keyed by name: two rows
|
|
14
|
+
* registering the same name throw, and two names carrying the same text are
|
|
15
|
+
* the duplication this module exists to remove.
|
|
16
|
+
* - Its text is a function of the tools actually mounted
|
|
17
|
+
* (`ctx.tools.get(name, scope)`, read at every assembly), which is what
|
|
18
|
+
* keeps git_bash and wsl_bash independently switchable: any subset — either
|
|
19
|
+
* one, both, or neither — renders exactly one correct copy, and a
|
|
20
|
+
* composition with neither renders nothing at all.
|
|
21
|
+
* - dsh's own tool-pwsh description repeats some of this prose and is not ours
|
|
22
|
+
* to edit. This section stays self-sufficient on purpose: our tools must
|
|
23
|
+
* carry their full guidance whether or not pwsh is in the composition, so we
|
|
24
|
+
* never read facts out of another row's description.
|
|
25
|
+
* @module dsh-win-multi-bash/tool-bash/shell-family
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The tool names this plugin contributes to the shell family. */
|
|
29
|
+
export const SHELL_FAMILY_TOOL_NAMES = ['git_bash', 'wsl_bash'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The single prompt section carrying the family's shared guidance. Named for
|
|
33
|
+
* the plugin's tool family rather than for one tool, so neither git_bash nor
|
|
34
|
+
* wsl_bash owns it.
|
|
35
|
+
*/
|
|
36
|
+
export const SHELL_FAMILY_SECTION_NAME = 'tool:win-mb-bash';
|
|
37
|
+
|
|
38
|
+
/** Fresh-shell fact plus the `workdir` rule that replaces a persistent `cd`. */
|
|
39
|
+
const SHELL_CALL_SECTION = 'Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`.';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cross-call exit-status guidance. It belongs in the prompt rather than the
|
|
43
|
+
* tool schema because the trap is not about one call's arguments but about how
|
|
44
|
+
* a multi-step command is composed: `;` never stops on failure, and a pipeline
|
|
45
|
+
* reports only its last command's status — so the reflex of bounding long
|
|
46
|
+
* output with `| tail`, which this runtime already truncates for the caller,
|
|
47
|
+
* returns `tail`'s status instead of the command's.
|
|
48
|
+
*/
|
|
49
|
+
export const SHELL_EXIT_STATUS_SECTION = 'Check the [exit code: N] marker on every bash result; investigate failures before moving on. Chain dependent steps with `&&` or `set -o pipefail`: `;` never stops on failure, and `cmd | tail` returns the status of `tail`, not of `cmd`.';
|
|
50
|
+
|
|
51
|
+
/** Managed `$DSH_*` environment facts are the same for every shell tool. */
|
|
52
|
+
const SHELL_HARNESS_FACTS_SECTION = 'Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed.';
|
|
53
|
+
|
|
54
|
+
/** The confinement marker one shared classifier produces for every dialect. */
|
|
55
|
+
const SHELL_SANDBOX_SECTION = 'Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way.';
|
|
56
|
+
|
|
57
|
+
/** Retention contract: the runtime truncates, so no call needs to bound output itself. */
|
|
58
|
+
const SHELL_OUTPUT_SECTION = 'Long output is truncated to its tail; the full output is saved to a file whose path is reported when available.';
|
|
59
|
+
|
|
60
|
+
/** Path-safety rules shared by every dialect (MSYS, Linux, and pwsh alike). */
|
|
61
|
+
const SHELL_PATH_SAFETY_SECTION = 'Before any delete or move, verify that the resolved absolute target path is the intended one; never run it against a computed path you have not checked. An unset variable expands to an empty string, so guard variables in such paths with `${VAR:?}`.';
|
|
62
|
+
|
|
63
|
+
/** Advertised only while some mounted family tool offers `run_in_background`. */
|
|
64
|
+
export const SHELL_BACKGROUND_SECTION = 'Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`.';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The same-turn escalation guidance appended after a denial marker. Kept in one
|
|
68
|
+
* place because every dialect shares the exact approval contract, and included
|
|
69
|
+
* only while some mounted family tool advertises `sandbox_permissions`. The
|
|
70
|
+
* opening clause names that dependency because the section speaks for the whole
|
|
71
|
+
* family: a tool that advertises no such parameter has nothing to escalate
|
|
72
|
+
* with, and its own schema is what says so.
|
|
73
|
+
*/
|
|
74
|
+
export const SHELL_ESCALATION_SECTION = 'Where a shell tool advertises `sandbox_permissions`: attempting a command the sandbox may deny is safe and expected — run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compose the family section from the facts that hold for the mounted set.
|
|
78
|
+
* @param advertised - what the mounted family tools advertise this composition.
|
|
79
|
+
* @param advertised.background - some mounted family tool offers `run_in_background`.
|
|
80
|
+
* @param advertised.escalation - some mounted family tool offers `sandbox_permissions`.
|
|
81
|
+
* @returns the section text, with no fact stated twice.
|
|
82
|
+
*/
|
|
83
|
+
export function shellFamilySectionText({ background, escalation }) {
|
|
84
|
+
return [
|
|
85
|
+
SHELL_CALL_SECTION,
|
|
86
|
+
SHELL_EXIT_STATUS_SECTION,
|
|
87
|
+
SHELL_HARNESS_FACTS_SECTION,
|
|
88
|
+
SHELL_SANDBOX_SECTION,
|
|
89
|
+
SHELL_OUTPUT_SECTION,
|
|
90
|
+
SHELL_PATH_SAFETY_SECTION,
|
|
91
|
+
...background ? [SHELL_BACKGROUND_SECTION] : [],
|
|
92
|
+
...escalation ? [SHELL_ESCALATION_SECTION] : [],
|
|
93
|
+
].join(' ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The Loader specifier of the row that owns the family section. After the row
|
|
98
|
+
* merge that is the package row itself — the row whose `name` is the bare
|
|
99
|
+
* package specifier, which is also the row the Web client needs in order to
|
|
100
|
+
* find `dsh.client` and serve the browser half. Exported so the tool rows can
|
|
101
|
+
* tell whether that owner is composed: the section is the only carrier of the
|
|
102
|
+
* shared guidance, and the tool descriptions were slimmed on that assumption,
|
|
103
|
+
* so their absence has to be detected rather than assumed away.
|
|
104
|
+
*/
|
|
105
|
+
export const SHELL_FAMILY_ROW_SPECIFIER = 'dsh-win-multi-bash';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the row that owns the shared shell prompt section is composed and
|
|
109
|
+
* enabled. The tool descriptions carry only their dialect facts on the
|
|
110
|
+
* assumption that section states the rest, so this is worth knowing: without
|
|
111
|
+
* that row the shared guidance would silently vanish from the prompt.
|
|
112
|
+
*
|
|
113
|
+
* The Loader tree lists declared rows with their effective enablement before
|
|
114
|
+
* they activate, so the answer does not depend on row order. A loader that
|
|
115
|
+
* cannot be read answers `undefined` (treat as composed) — the default
|
|
116
|
+
* composition has the row, and guessing the other way would duplicate prose.
|
|
117
|
+
* @param ctx - a context that can reach the Loader (a row's own context).
|
|
118
|
+
* @returns true/false when known, `undefined` when the loader is unreadable.
|
|
119
|
+
*/
|
|
120
|
+
export function familyRowComposed(ctx) {
|
|
121
|
+
const loader = ctx.get?.('loader') ?? ctx.loader;
|
|
122
|
+
if (loader === undefined || typeof loader.entries !== 'function') return undefined;
|
|
123
|
+
for (const entry of loader.entries()) {
|
|
124
|
+
if (entry.options?.name !== SHELL_FAMILY_ROW_SPECIFIER) continue;
|
|
125
|
+
return entry.disabled !== true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The family section text for one assembly, resolved from the tools actually
|
|
132
|
+
* mounted: it states nothing about a tool that is not there (a tool that
|
|
133
|
+
* advertises no `sandbox_permissions` also gets no escalation paragraph), and
|
|
134
|
+
* with no family tool mounted it renders no text at all — the section is then
|
|
135
|
+
* empty and dropped from the rendered prompt.
|
|
136
|
+
* @param ctx - a context with the `tools` service.
|
|
137
|
+
* @param scope - the assembly scope `tools.get` takes.
|
|
138
|
+
* @returns the section text, or `''` when this composition mounts no family tool.
|
|
139
|
+
*/
|
|
140
|
+
export function shellFamilySectionTextFor(ctx, scope) {
|
|
141
|
+
const mounted = SHELL_FAMILY_TOOL_NAMES
|
|
142
|
+
.map((toolName) => ctx.tools.get(toolName, scope))
|
|
143
|
+
.filter((definition) => definition !== undefined);
|
|
144
|
+
if (mounted.length === 0) return '';
|
|
145
|
+
const advertises = (parameter) => mounted.some((definition) => definition.parameters?.properties?.[parameter] !== undefined);
|
|
146
|
+
return shellFamilySectionText({
|
|
147
|
+
background: advertises('run_in_background'),
|
|
148
|
+
escalation: advertises('sandbox_permissions'),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* One tool's model-facing description. Normally that is only its dialect facts —
|
|
154
|
+
* the family section carries the rest once for the whole composition. When the
|
|
155
|
+
* owning row is **not** composed (a user switched it off, or wired a subset of
|
|
156
|
+
* the patch by hand), the shared text falls back into this description instead,
|
|
157
|
+
* so the model never silently loses the exit-marker, sandbox, truncation,
|
|
158
|
+
* path-safety, background and escalation guidance. The duplicated prose is the
|
|
159
|
+
* price of that fallback, and it only exists in a configuration that has no
|
|
160
|
+
* section at all.
|
|
161
|
+
* @param own - the dialect-owned description from {@link shellDescription}.
|
|
162
|
+
* @param familyPresent - whether the section's owning row is composed and enabled;
|
|
163
|
+
* only a definite `false` falls back, because an unknown state must never
|
|
164
|
+
* duplicate prose the section would also state.
|
|
165
|
+
* @param advertised - what this tool advertises, as {@link shellFamilySectionText} takes.
|
|
166
|
+
* @returns the description to register.
|
|
167
|
+
*/
|
|
168
|
+
export function composeToolDescription({ own, familyPresent, background, escalation }) {
|
|
169
|
+
if (familyPresent !== false) return own;
|
|
170
|
+
return `${own} ${shellFamilySectionText({ background, escalation })}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Midpoint of the two placements dsh centrally allocates to shell tools, so the
|
|
175
|
+
* family section sorts between `tool:bash` and `tool:pwsh` instead of ahead of
|
|
176
|
+
* every other section. Resolved from the registry rather than hardcoded; the
|
|
177
|
+
* literals below are only the fallback for a registry that stopped exposing
|
|
178
|
+
* both names (the midpoint of TOOL_BASH 1000 / TOOL_PWSH 1010 at the time of
|
|
179
|
+
* writing).
|
|
180
|
+
* @param systemPrompt - the prompt registry the section is registered on.
|
|
181
|
+
* @returns a finite order value strictly between TOOL_BASH and TOOL_PWSH.
|
|
182
|
+
*/
|
|
183
|
+
export function shellFamilySectionOrder(systemPrompt) {
|
|
184
|
+
const bash = systemPrompt.getSectionOrder('TOOL_BASH');
|
|
185
|
+
const pwsh = systemPrompt.getSectionOrder('TOOL_PWSH');
|
|
186
|
+
if (Number.isFinite(bash) && Number.isFinite(pwsh) && pwsh > bash)
|
|
187
|
+
return (bash + pwsh) / 2;
|
|
188
|
+
if (Number.isFinite(bash))
|
|
189
|
+
return bash + 1;
|
|
190
|
+
if (Number.isFinite(pwsh))
|
|
191
|
+
return pwsh - 1;
|
|
192
|
+
return 1005;
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-win-multi-bash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"author": "Dinosaur_MC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"main": "lib/index.js",
|
|
27
27
|
"exports": {
|
|
28
28
|
".": "./lib/index.js",
|
|
29
|
+
"./client": "./lib/client.js",
|
|
29
30
|
"./tool-git-bash": "./lib/tool-bash/types/git-bash.js",
|
|
30
31
|
"./tool-wsl-bash": "./lib/tool-bash/types/wsl-bash.js",
|
|
31
32
|
"./package.json": "./package.json"
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
"files": [
|
|
34
35
|
"lib/**/*.js",
|
|
35
36
|
"cordis.patch.yml",
|
|
37
|
+
"install.ps1",
|
|
38
|
+
"uninstall.ps1",
|
|
36
39
|
"README.md",
|
|
37
40
|
"README.zh.md",
|
|
38
41
|
"README.i18n.yaml",
|
|
@@ -45,6 +48,15 @@
|
|
|
45
48
|
"dsh": {
|
|
46
49
|
"bundle": {
|
|
47
50
|
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"platform": "web",
|
|
54
|
+
"inject": [
|
|
55
|
+
"@deepseek-ai/dsh-client-locale",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
59
|
+
]
|
|
48
60
|
}
|
|
49
61
|
},
|
|
50
62
|
"peerDependencies": {
|
package/uninstall.ps1
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
热拔 dsh-win-multi-bash(自包含版):从 profile 的 cordis.patch.yml 删除
|
|
4
|
+
managed 块,并移除 profile node_modules 里的插件 junction。
|
|
5
|
+
dsh web 会热重载,移除后新会话不再有 git_bash / wsl_bash 工具。
|
|
6
|
+
|
|
7
|
+
.PARAMETER ProfileName
|
|
8
|
+
目标 profile 名,默认 web。
|
|
9
|
+
#>
|
|
10
|
+
[CmdletBinding()]
|
|
11
|
+
param(
|
|
12
|
+
[string]$ProfileName = 'web'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
|
|
17
|
+
$dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
|
|
18
|
+
$patchPath = Join-Path $dshHome "profiles\$ProfileName\cordis.patch.yml"
|
|
19
|
+
$linkPath = Join-Path $dshHome "profiles\$ProfileName\node_modules\dsh-win-multi-bash"
|
|
20
|
+
|
|
21
|
+
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
|
22
|
+
$removed = $false
|
|
23
|
+
|
|
24
|
+
if (Test-Path $patchPath) {
|
|
25
|
+
$existing = [System.IO.File]::ReadAllText($patchPath, $utf8NoBom)
|
|
26
|
+
$pattern = '(?s)# --- dsh-win-multi-bash managed.*?# --- end dsh-win-multi-bash managed ---\r?\n?'
|
|
27
|
+
$removed = $existing -match $pattern
|
|
28
|
+
if ($removed) {
|
|
29
|
+
[System.IO.File]::WriteAllText($patchPath, [regex]::Replace($existing, $pattern, ''), $utf8NoBom)
|
|
30
|
+
Write-Host "[dsh-win-multi-bash] 已从 $patchPath 移除 managed 块。热重载后新会话不再有 git_bash / wsl_bash 工具。"
|
|
31
|
+
} else {
|
|
32
|
+
Write-Host "[dsh-win-multi-bash] $patchPath 中没有 managed 块。"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Test-Path $linkPath) {
|
|
37
|
+
$item = Get-Item $linkPath -Force
|
|
38
|
+
if ($item.LinkType -eq 'Junction') {
|
|
39
|
+
# PS 5.1 的 Remove-Item 删 junction 会抛 NullReferenceException(已知 bug),
|
|
40
|
+
# 用 cmd rmdir(只删链接本身,绝不触碰链接目标)。
|
|
41
|
+
cmd /c rmdir "$linkPath"
|
|
42
|
+
Write-Host "[dsh-win-multi-bash] 已移除插件 junction: $linkPath"
|
|
43
|
+
} else {
|
|
44
|
+
Write-Host "[dsh-win-multi-bash] $linkPath 存在但不是 junction,未动(请手动处理)"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (-not $removed -and -not (Test-Path $linkPath)) {
|
|
49
|
+
Write-Host "[dsh-win-multi-bash] 若你是用 bundle 路径安装的(dsh plugin add),请运行: dsh plugin --profile $ProfileName remove dsh-win-multi-bash"
|
|
50
|
+
}
|