@deepseek-ai/dsh-pwsh-local 0.1.6-alpha.2 → 0.1.7-alpha.2
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 +6 -5
- package/README.zh.md +6 -5
- package/lib/index.js +161 -196
- package/lib/types/index.d.ts +41 -32
- package/package.json +11 -13
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/shell/pwsh-local/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: f4001263ba06d02a4b4c3b801d9a518488a97327
|
|
6
|
+
README.zh.md: e9e1c3246bef62f55c965d4974202646b5fc2096
|
package/README.md
CHANGED
|
@@ -57,21 +57,22 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
|
|
57
57
|
|
|
58
58
|
### Running commands
|
|
59
59
|
|
|
60
|
-
Run a command
|
|
60
|
+
Run a command by awaiting the execution's `result()` projection; a nonzero exit, a timeout, or a cancellation resolves descriptively, and only infrastructure failures reject. The command string rides as one argument to `-Command`: PowerShell parses the text itself and no intermediate shell exists, so there is no shell-quoting layer to escape and native Win32 paths pass through unchanged. Every command pins UTF-8 output first, so non-ASCII output is not garbled even on the Windows PowerShell 5.1 fallback. The environment is model-friendly: `NO_COLOR=1 PAGER=cat GIT_PAGER=cat` (no `TERM=dumb` — a POSIX concept), with explicit caller-provided entries still winning.
|
|
61
61
|
|
|
62
62
|
```text
|
|
63
|
-
const
|
|
63
|
+
const execution = await ctx.shell.execute(ctx.shell.resolve({ command: 'Get-ChildItem' }))
|
|
64
|
+
const result = await execution.result()
|
|
64
65
|
if (result.timedOut) console.log('timed out after', result.timeoutMs)
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
### Background processes
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
Resolve with `onExpiry: 'none'` and await `execute` to run a command in the background; no deadline is armed. Cancellation or preparation failure rejects before a handle is published. `readOutput()` merges the stream deltas into one consuming read, marking stderr under a `[stderr]` section; `kill()` terminates the provider-managed range; `done` settles when the direct command closes and never rejects. Job ids, ownership, polling, and notices belong to the generic `ctx.jobs` runtime, which the tool layer registers the handle with.
|
|
70
71
|
|
|
71
72
|
<a id="adjusting-budgets-at-runtime"></a>
|
|
72
73
|
### Adjusting budgets at runtime
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
Execution budgets are volatile Config fields sampled when resolving each command. The Plugins page edits the active executor’s profile entry. Complete Config validation rejects invalid numbers and timer limits before a form write reaches disk.
|
|
75
76
|
|
|
76
77
|
-----
|
|
77
78
|
|
|
@@ -145,7 +146,7 @@ These limits define when this executor is a poor fit. They are current package c
|
|
|
145
146
|
- **Unconfined by itself** — commands run with the harness process's authority; deployments needing confinement compose a sandboxing executor or policy instead.
|
|
146
147
|
- **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`.
|
|
147
148
|
- **The command string is PowerShell text** — the `-Command` domain has no shell-quoting layer, but a model-facing command is parsed by PowerShell itself, so PowerShell syntax errors are command failures, not launch failures.
|
|
148
|
-
- **A background provider-failure note is
|
|
149
|
+
- **A background provider-failure note is the whole stderr stream** — `SubprocessHandle.done` can reject before or after target execution begins, and the subprocess service buffers no output for a target that never reported, so the executor serves the stage-neutral `subprocess failed before reporting an outcome: …` as the observed stderr stream (offset readers re-read it at their own offsets) and folds it into exactly one `readOutput()` delta; a consuming reader that discards that delta recovers it only through `observed.stderr`.
|
|
149
150
|
- **Windows termination reports no signal** — a force-killed process settles as exit 1 with `signal: null`, so signal-based status classification does not apply on Windows; `kill()`-initiated stops still stamp `killed` directly.
|
|
150
151
|
- **The encoding preamble precedes the command** — PowerShell requires `param(...)`, `#requires`, and `using` statements at the very top of a script, so a command whose first statement is one of those cannot run under the UTF-8 output preamble; wrap a `param(...)` script in `& { … }`, and run `using`/`#requires` scripts from a file instead.
|
|
151
152
|
- **Non-ASCII stdin under Windows PowerShell 5.1 may be mis-decoded** — the preamble pins output encoding only; `[Console]::InputEncoding` stays at the host default because setting it under redirected stdin throws; pwsh 7 defaults to UTF-8 and is unaffected.
|
package/README.zh.md
CHANGED
|
@@ -57,21 +57,22 @@ kind: "package-reference"
|
|
|
57
57
|
|
|
58
58
|
### 运行命令
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
通过等待执行句柄的 `result()` 投影来运行命令;非零退出、超时或取消都会返回描述性结果,只有基础设施失败才会导致调用被拒绝。命令字符串作为单个参数传给 `-Command`:由 PowerShell 自己解析文本,不存在中间 shell,因此没有需要转义的 shell 引号层,原生 Win32 路径也原样通过。每条命令都先固定 UTF-8 输出,因此即使在 Windows PowerShell 5.1 兜底上,非 ASCII 输出也不会乱码。环境默认面向模型:`NO_COLOR=1 PAGER=cat GIT_PAGER=cat`(没有 `TERM=dumb`——那是 POSIX 概念),调用方显式提供的条目仍然优先。
|
|
61
61
|
|
|
62
62
|
```text
|
|
63
|
-
const
|
|
63
|
+
const execution = await ctx.shell.execute(ctx.shell.resolve({ command: 'Get-ChildItem' }))
|
|
64
|
+
const result = await execution.result()
|
|
64
65
|
if (result.timedOut) console.log('timed out after', result.timeoutMs)
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
### 后台进程
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
以 `onExpiry: 'none'` 解析并等待 `execute` 返回句柄即可在后台运行命令;不布置任何 deadline。取消或准备失败会在发布句柄前拒绝调用。`readOutput()` 把流增量合并为一次消费式读取,并在 `[stderr]` 分段下标记 stderr;`kill()` 终止提供方管理的 range;`done` 在直接命令关闭时结算且绝不 reject。job id、所有权、轮询与通知属于通用 `ctx.jobs` 运行时,工具层会把句柄注册进去。
|
|
70
71
|
|
|
71
72
|
<a id="adjusting-budgets-at-runtime"></a>
|
|
72
73
|
### 运行时调整预算
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
执行预算是解析每条命令时读取的 volatile Config 字段。插件页面编辑当前执行器的 profile 条目。完整 Config 验证在表单写入磁盘前拒绝无效数字和定时器上限。
|
|
75
76
|
|
|
76
77
|
-----
|
|
77
78
|
|
|
@@ -145,7 +146,7 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
|
|
|
145
146
|
- **自身不提供隔离**——命令以 harness 进程的权限运行;需要隔离的部署组合沙箱执行器或策略。
|
|
146
147
|
- **没有持久 shell 或 PTY**——每次调用都启动全新的 `pwsh -Command`。
|
|
147
148
|
- **命令字符串是 PowerShell 文本**——`-Command` 域没有 shell 引号层,但面向模型的命令由 PowerShell 自己解析,因此 PowerShell 语法错误是命令失败,而非启动失败。
|
|
148
|
-
-
|
|
149
|
+
- **后台提供方失败提示就是整条 stderr 流**——`SubprocessHandle.done` 可能在目标命令开始执行前或后被拒绝,而 subprocess 服务不会为从未上报结果的目标命令 缓冲任何输出,因此执行器把不声明失败阶段的 `subprocess failed before reporting an outcome: …` 作为观测到的 stderr 流提供(偏移读取方按各自偏移重读),并只折入恰好一个 `readOutput()` 增量;丢弃了该增量的消耗式读取方只能经 `observed.stderr` 恢复它。
|
|
149
150
|
- **Windows 终止不报告信号**——被强制终止的进程以退出码 1、`signal: null` 结算,因此基于信号的状态分类在 Windows 上不适用;`kill()` 发起的停止仍会直接标记为 `killed`。
|
|
150
151
|
- **编码 preamble 位于命令之前**——PowerShell 要求 `param(...)`、`#requires` 与 `using` 语句位于脚本最顶部,因此以其中一种开头的命令无法在 UTF-8 输出 preamble 下运行;`param(...)` 脚本请包进 `& { … }`,`using`/`#requires` 脚本请改从文件运行。
|
|
151
152
|
- **Windows PowerShell 5.1 下的非 ASCII stdin 可能被错误解码**——preamble 只固定输出编码;`[Console]::InputEncoding` 保持主机默认,因为在重定向 stdin 下设置它会抛出异常;pwsh 7 默认 UTF-8,不受影响。
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import {
|
|
2
|
+
import { ShellExecutor } from "@deepseek-ai/dsh-shell";
|
|
3
3
|
import { MAX_TIMER_DELAY_MS, clampTimeout, deadline, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
4
4
|
import { lstatSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -79,64 +79,6 @@ function resolvePwshPath(configured, env = process.env, platform = process.platf
|
|
|
79
79
|
*
|
|
80
80
|
* @module @deepseek-ai/dsh-pwsh-local
|
|
81
81
|
*/
|
|
82
|
-
var __addDisposableResource = function(env, value, async) {
|
|
83
|
-
if (value !== null && value !== void 0) {
|
|
84
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
85
|
-
var dispose, inner;
|
|
86
|
-
if (async) {
|
|
87
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
88
|
-
dispose = value[Symbol.asyncDispose];
|
|
89
|
-
}
|
|
90
|
-
if (dispose === void 0) {
|
|
91
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
92
|
-
dispose = value[Symbol.dispose];
|
|
93
|
-
if (async) inner = dispose;
|
|
94
|
-
}
|
|
95
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
96
|
-
if (inner) dispose = function() {
|
|
97
|
-
try {
|
|
98
|
-
inner.call(this);
|
|
99
|
-
} catch (e) {
|
|
100
|
-
return Promise.reject(e);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
env.stack.push({
|
|
104
|
-
value,
|
|
105
|
-
dispose,
|
|
106
|
-
async
|
|
107
|
-
});
|
|
108
|
-
} else if (async) env.stack.push({ async: true });
|
|
109
|
-
return value;
|
|
110
|
-
};
|
|
111
|
-
var __disposeResources = (function(SuppressedError) {
|
|
112
|
-
return function(env) {
|
|
113
|
-
function fail(e) {
|
|
114
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
115
|
-
env.hasError = true;
|
|
116
|
-
}
|
|
117
|
-
var r, s = 0;
|
|
118
|
-
function next() {
|
|
119
|
-
while (r = env.stack.pop()) try {
|
|
120
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
121
|
-
if (r.dispose) {
|
|
122
|
-
var result = r.dispose.call(r.value);
|
|
123
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
|
|
124
|
-
fail(e);
|
|
125
|
-
return next();
|
|
126
|
-
});
|
|
127
|
-
} else s |= 1;
|
|
128
|
-
} catch (e) {
|
|
129
|
-
fail(e);
|
|
130
|
-
}
|
|
131
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
132
|
-
if (env.hasError) throw env.error;
|
|
133
|
-
}
|
|
134
|
-
return next();
|
|
135
|
-
};
|
|
136
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
|
137
|
-
var e = new Error(message);
|
|
138
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
139
|
-
});
|
|
140
82
|
/**
|
|
141
83
|
* Model-friendly environment overrides for PowerShell: disable colors and
|
|
142
84
|
* pagers that would garble tool output. `TERM=dumb` is a POSIX concept and is
|
|
@@ -175,19 +117,17 @@ function assertPositiveFinite(name, value) {
|
|
|
175
117
|
/**
|
|
176
118
|
* Reject a resolved section this executor could not run with. The schema
|
|
177
119
|
* expresses neither "positive and finite" nor the timer bound `graceMs` has to
|
|
178
|
-
* fit, so a stored value
|
|
179
|
-
* the
|
|
180
|
-
* @param config - the resolved section, schema-valid by construction.
|
|
120
|
+
* fit, so a stored value that cannot be used fails at the next command.
|
|
121
|
+
* @param config - the live configuration, schema-valid by construction.
|
|
181
122
|
* @throws Error naming the field that cannot be used.
|
|
182
123
|
*/
|
|
183
124
|
function assertServiceablePwshConfig(config) {
|
|
184
|
-
|
|
185
|
-
assertPositiveFinite("
|
|
186
|
-
assertPositiveFinite("
|
|
187
|
-
assertPositiveFinite("
|
|
188
|
-
assertPositiveFinite("
|
|
189
|
-
|
|
190
|
-
if (resolved.graceMs > MAX_TIMER_DELAY_MS) throw new Error(`pwsh-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
125
|
+
assertPositiveFinite("timeoutMs", config.timeoutMs.get());
|
|
126
|
+
assertPositiveFinite("maxTimeoutMs", config.maxTimeoutMs.get());
|
|
127
|
+
assertPositiveFinite("maxOutputBytes", config.maxOutputBytes.get());
|
|
128
|
+
assertPositiveFinite("maxSpillBytes", config.maxSpillBytes.get());
|
|
129
|
+
assertPositiveFinite("graceMs", config.graceMs.get());
|
|
130
|
+
if (config.graceMs.get() > MAX_TIMER_DELAY_MS) throw new Error(`pwsh-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
191
131
|
}
|
|
192
132
|
/**
|
|
193
133
|
* Local PowerShell executor over `ctx.subprocess`. Bounded output, spill
|
|
@@ -195,51 +135,35 @@ function assertServiceablePwshConfig(config) {
|
|
|
195
135
|
* this executor supplies their configured budgets per spawn.
|
|
196
136
|
*/
|
|
197
137
|
var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
138
|
+
config;
|
|
198
139
|
static inject = ["subprocess"];
|
|
199
140
|
static Config = z.object({
|
|
200
|
-
cwd: z.string(),
|
|
201
|
-
timeoutMs: z.number().default(12e4),
|
|
202
|
-
maxTimeoutMs: z.number().default(6e5),
|
|
203
|
-
maxOutputBytes: z.number().default(64e3),
|
|
204
|
-
maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES),
|
|
205
|
-
graceMs: z.number().default(DEFAULT_GRACE_MS),
|
|
206
|
-
pwshPath: z.string()
|
|
141
|
+
cwd: z.string().volatile(),
|
|
142
|
+
timeoutMs: z.number().default(12e4).volatile(),
|
|
143
|
+
maxTimeoutMs: z.number().default(6e5).volatile(),
|
|
144
|
+
maxOutputBytes: z.number().default(64e3).volatile(),
|
|
145
|
+
maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES).volatile(),
|
|
146
|
+
graceMs: z.number().default(DEFAULT_GRACE_MS).volatile(),
|
|
147
|
+
pwshPath: z.string().volatile()
|
|
207
148
|
});
|
|
208
|
-
/** The currently authoritative config: the settings section, or the composition entry. */
|
|
209
|
-
source;
|
|
210
149
|
/** The declared executable the current {@link pwshPath} was resolved from. */
|
|
211
150
|
declaredPwshPath;
|
|
212
151
|
/** The pwsh executable resolved from the current config. */
|
|
213
152
|
resolvedPwshPath;
|
|
214
|
-
/**
|
|
215
|
-
get config() {
|
|
216
|
-
return this.source();
|
|
217
|
-
}
|
|
218
|
-
/** The pwsh executable every command runs through. */
|
|
153
|
+
/** The pwsh executable every command runs through; a changed declared path is probed again on the next read. */
|
|
219
154
|
get pwshPath() {
|
|
155
|
+
const declared = this.config.pwshPath.get();
|
|
156
|
+
if (declared !== this.declaredPwshPath) {
|
|
157
|
+
this.resolvedPwshPath = resolvePwshPath(declared);
|
|
158
|
+
this.declaredPwshPath = declared;
|
|
159
|
+
}
|
|
220
160
|
return this.resolvedPwshPath;
|
|
221
161
|
}
|
|
222
162
|
constructor(ctx, config) {
|
|
223
163
|
super(ctx);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.
|
|
227
|
-
this.declaredPwshPath = entry.pwshPath;
|
|
228
|
-
this.resolvedPwshPath = resolvePwshPath(entry.pwshPath);
|
|
229
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
230
|
-
settingsCtx.settings.installSection(ctx, SHELL_SETTINGS_NAMESPACE, PwshLocalExecutor.Config, entry, {
|
|
231
|
-
validate: assertServiceablePwshConfig,
|
|
232
|
-
setSource: (current) => {
|
|
233
|
-
this.source = current;
|
|
234
|
-
},
|
|
235
|
-
onChange: () => {
|
|
236
|
-
const declared = this.source().pwshPath;
|
|
237
|
-
if (declared === this.declaredPwshPath) return;
|
|
238
|
-
this.declaredPwshPath = declared;
|
|
239
|
-
this.resolvedPwshPath = resolvePwshPath(declared);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
});
|
|
164
|
+
this.config = config;
|
|
165
|
+
this.declaredPwshPath = config.pwshPath.get();
|
|
166
|
+
this.resolvedPwshPath = resolvePwshPath(this.declaredPwshPath);
|
|
243
167
|
}
|
|
244
168
|
/**
|
|
245
169
|
* Resolve a request into a fully-specified spec: fill `workdir` from
|
|
@@ -247,13 +171,15 @@ var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
|
247
171
|
* `config.timeoutMs`, capped at `config.maxTimeoutMs`.
|
|
248
172
|
*/
|
|
249
173
|
resolve(request) {
|
|
250
|
-
|
|
251
|
-
const
|
|
174
|
+
assertServiceablePwshConfig(this.config);
|
|
175
|
+
const timeoutMs = clampTimeout(request.timeoutMs, this.config.timeoutMs.get(), this.config.maxTimeoutMs.get(), "pwsh-local: request.timeoutMs");
|
|
176
|
+
const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes.get();
|
|
252
177
|
assertPositiveFinite("request.stdoutMaxBytes", stdoutMaxBytes);
|
|
253
178
|
return {
|
|
254
179
|
command: request.command,
|
|
255
|
-
workdir: request.workdir ?? this.config.cwd ?? process.cwd(),
|
|
180
|
+
workdir: request.workdir ?? this.config.cwd.get() ?? process.cwd(),
|
|
256
181
|
timeoutMs,
|
|
182
|
+
onExpiry: request.onExpiry ?? "kill",
|
|
257
183
|
stdoutMaxBytes,
|
|
258
184
|
...request.signal ? { signal: request.signal } : {},
|
|
259
185
|
...request.stdin !== void 0 ? { stdin: request.stdin } : {},
|
|
@@ -265,7 +191,7 @@ var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
|
265
191
|
/**
|
|
266
192
|
* The pwsh invocation argv for one resolved spec — the argv-level seam a
|
|
267
193
|
* confining subclass wraps through `ctx.sandbox.confine` (the pwsh twin of
|
|
268
|
-
* `dsh-bash-local`'s `
|
|
194
|
+
* `dsh-bash-local`'s `executeArgv` hook; see
|
|
269
195
|
* `@deepseek-ai/dsh-pwsh-sandbox`).
|
|
270
196
|
*/
|
|
271
197
|
argv(spec) {
|
|
@@ -282,7 +208,7 @@ var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
|
282
208
|
spawnSpec(spec, stdoutMaxBytes, signal, argv) {
|
|
283
209
|
const collect = (maxBytes) => ({
|
|
284
210
|
maxBytes,
|
|
285
|
-
spill: { maxBytes: this.config.maxSpillBytes }
|
|
211
|
+
spill: { maxBytes: this.config.maxSpillBytes.get() }
|
|
286
212
|
});
|
|
287
213
|
return {
|
|
288
214
|
argv: [...argv],
|
|
@@ -290,9 +216,9 @@ var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
|
290
216
|
stdio: {
|
|
291
217
|
stdin: spec.stdin !== void 0 ? { data: spec.stdin } : "ignore",
|
|
292
218
|
stdout: collect(stdoutMaxBytes),
|
|
293
|
-
stderr: collect(this.config.maxOutputBytes)
|
|
219
|
+
stderr: collect(this.config.maxOutputBytes.get())
|
|
294
220
|
},
|
|
295
|
-
graceMs: this.config.graceMs,
|
|
221
|
+
graceMs: this.config.graceMs.get(),
|
|
296
222
|
signal,
|
|
297
223
|
env: {
|
|
298
224
|
...ENV_OVERRIDES,
|
|
@@ -312,116 +238,140 @@ var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
|
312
238
|
stderr
|
|
313
239
|
};
|
|
314
240
|
}
|
|
315
|
-
async
|
|
316
|
-
return
|
|
241
|
+
async execute(spec) {
|
|
242
|
+
return this.executeArgv(spec, this.argv(spec));
|
|
317
243
|
}
|
|
318
244
|
/**
|
|
319
|
-
*
|
|
245
|
+
* Execute an explicit argv with the lifecycle, environment, output,
|
|
246
|
+
* deadline, and cancellation semantics of this executor. Subclasses use this
|
|
247
|
+
* after replacing the public command's shell argv at an execution boundary.
|
|
320
248
|
* @param spec - resolved execution settings and caller-owned command metadata.
|
|
321
|
-
* @param argvOrPrepare - exact argv, or preparation
|
|
322
|
-
* @
|
|
249
|
+
* @param argvOrPrepare - exact argv, or preparation using the execution cancellation signal.
|
|
250
|
+
* @param onStarted - installs provider facts synchronously before the handle can settle.
|
|
251
|
+
* @returns the live execution handle; spawn rejection settles the handle as
|
|
252
|
+
* killed while `result()` carries the same failure as its rejection.
|
|
323
253
|
*/
|
|
324
|
-
async
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const cancelled = Promise.withResolvers();
|
|
335
|
-
const abort = () => {
|
|
336
|
-
cancelled.reject(d.signal.reason);
|
|
337
|
-
};
|
|
338
|
-
d.signal.addEventListener("abort", abort, { once: true });
|
|
339
|
-
try {
|
|
340
|
-
argv = await Promise.race([Promise.resolve().then(() => {
|
|
341
|
-
d.signal.throwIfAborted();
|
|
342
|
-
return argvOrPrepare(d.signal);
|
|
343
|
-
}), cancelled.promise]);
|
|
344
|
-
d.signal.throwIfAborted();
|
|
345
|
-
} catch (error) {
|
|
346
|
-
if (timeoutOf(d.signal, "BASH_TIMEOUT") === void 0) throw error;
|
|
347
|
-
return {
|
|
348
|
-
spawnRequested: false,
|
|
349
|
-
result: {
|
|
350
|
-
exitCode: null,
|
|
351
|
-
signal: null,
|
|
352
|
-
timedOut: true,
|
|
353
|
-
aborted: false,
|
|
354
|
-
timeoutMs: spec.timeoutMs,
|
|
355
|
-
stdout: {
|
|
356
|
-
text: "",
|
|
357
|
-
truncated: false
|
|
358
|
-
},
|
|
359
|
-
stderr: {
|
|
360
|
-
text: "",
|
|
361
|
-
truncated: false
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
} finally {
|
|
366
|
-
d.signal.removeEventListener("abort", abort);
|
|
367
|
-
}
|
|
368
|
-
} else argv = argvOrPrepare;
|
|
369
|
-
const handle = this.ctx.subprocess.spawn(this.spawnSpec(spec, spec.stdoutMaxBytes, d.signal, argv));
|
|
370
|
-
const outcome = await handle.done;
|
|
371
|
-
const collected = PwshLocalExecutor.collected(handle);
|
|
372
|
-
const timedOut = timeoutOf(d.signal, "BASH_TIMEOUT") !== void 0;
|
|
373
|
-
const aborted = d.signal.aborted && !timedOut;
|
|
374
|
-
return {
|
|
375
|
-
spawnRequested: true,
|
|
376
|
-
result: {
|
|
377
|
-
...outcome,
|
|
254
|
+
async executeArgv(spec, argvOrPrepare, onStarted) {
|
|
255
|
+
let spawnSignal;
|
|
256
|
+
let classify;
|
|
257
|
+
let disarm = () => {};
|
|
258
|
+
if (spec.onExpiry === "kill") {
|
|
259
|
+
const d = deadline(spec.signal, spec.timeoutMs, "BASH_TIMEOUT");
|
|
260
|
+
spawnSignal = d.signal;
|
|
261
|
+
classify = () => {
|
|
262
|
+
const timedOut = timeoutOf(d.signal, "BASH_TIMEOUT") !== void 0;
|
|
263
|
+
return {
|
|
378
264
|
timedOut,
|
|
379
|
-
aborted
|
|
380
|
-
|
|
381
|
-
stdout: finalOutput(collected.stdout),
|
|
382
|
-
stderr: finalOutput(collected.stderr)
|
|
383
|
-
}
|
|
265
|
+
aborted: d.signal.aborted && !timedOut
|
|
266
|
+
};
|
|
384
267
|
};
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
268
|
+
disarm = () => {
|
|
269
|
+
d[Symbol.dispose]();
|
|
270
|
+
};
|
|
271
|
+
} else {
|
|
272
|
+
spawnSignal = spec.signal;
|
|
273
|
+
classify = () => ({
|
|
274
|
+
timedOut: false,
|
|
275
|
+
aborted: spec.signal?.aborted === true
|
|
276
|
+
});
|
|
390
277
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
278
|
+
let argv = [];
|
|
279
|
+
let preparationTimedOut = false;
|
|
280
|
+
if (typeof argvOrPrepare === "function") {
|
|
281
|
+
const signal = spawnSignal ?? new AbortController().signal;
|
|
282
|
+
const cancelled = Promise.withResolvers();
|
|
283
|
+
const abort = () => {
|
|
284
|
+
cancelled.reject(signal.reason);
|
|
285
|
+
};
|
|
286
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
287
|
+
try {
|
|
288
|
+
argv = await Promise.race([Promise.resolve().then(() => {
|
|
289
|
+
signal.throwIfAborted();
|
|
290
|
+
return argvOrPrepare(signal);
|
|
291
|
+
}), cancelled.promise]);
|
|
292
|
+
signal.throwIfAborted();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (!classify().timedOut) {
|
|
295
|
+
disarm();
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
preparationTimedOut = true;
|
|
299
|
+
} finally {
|
|
300
|
+
signal.removeEventListener("abort", abort);
|
|
301
|
+
}
|
|
302
|
+
} else argv = argvOrPrepare;
|
|
303
|
+
let running;
|
|
304
|
+
let syncSpawnError;
|
|
305
|
+
try {
|
|
306
|
+
if (!preparationTimedOut) running = this.ctx.subprocess.spawn(this.spawnSpec(spec, spec.stdoutMaxBytes, spawnSignal, argv));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
syncSpawnError = { error };
|
|
309
|
+
}
|
|
310
|
+
const emptyReader = { readFrom: () => ({
|
|
311
|
+
text: "",
|
|
312
|
+
lossy: false,
|
|
313
|
+
nextOffset: 0
|
|
314
|
+
}) };
|
|
315
|
+
const collected = running !== void 0 ? PwshLocalExecutor.collected(running) : {
|
|
316
|
+
stdout: emptyReader,
|
|
317
|
+
stderr: emptyReader
|
|
318
|
+
};
|
|
319
|
+
const spawnThrow = () => syncSpawnError.error;
|
|
320
|
+
const spawned = preparationTimedOut ? Promise.resolve({
|
|
321
|
+
exitCode: null,
|
|
322
|
+
signal: null
|
|
323
|
+
}) : running !== void 0 ? running.done : Promise.reject(spawnThrow());
|
|
324
|
+
let providerFailure;
|
|
325
|
+
let providerFailureReported = false;
|
|
401
326
|
const consumeProviderFailure = () => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return note;
|
|
327
|
+
if (providerFailure === void 0 || providerFailureReported) return "";
|
|
328
|
+
providerFailureReported = true;
|
|
329
|
+
return providerFailure.note;
|
|
405
330
|
};
|
|
331
|
+
const observedStderr = { readFrom: (fromByte) => {
|
|
332
|
+
if (providerFailure === void 0) return collected.stderr.readFrom(fromByte);
|
|
333
|
+
const note = Buffer.from(providerFailure.note, "utf8");
|
|
334
|
+
return {
|
|
335
|
+
text: note.subarray(Math.min(fromByte, note.length)).toString("utf8"),
|
|
336
|
+
nextOffset: note.length,
|
|
337
|
+
lossy: false
|
|
338
|
+
};
|
|
339
|
+
} };
|
|
406
340
|
let stdoutOffset = 0;
|
|
407
341
|
let stderrOffset = 0;
|
|
342
|
+
let resultPromise;
|
|
408
343
|
const proc = {
|
|
409
344
|
status: "running",
|
|
410
345
|
exitCode: null,
|
|
411
346
|
signal: null,
|
|
412
|
-
|
|
413
|
-
|
|
347
|
+
observed: {
|
|
348
|
+
stdout: collected.stdout,
|
|
349
|
+
stderr: observedStderr
|
|
350
|
+
},
|
|
351
|
+
done: spawned.then((outcome) => {
|
|
352
|
+
if (proc.status === "running") proc.status = spawnSignal?.aborted === true || outcome.signal !== null ? "killed" : "completed";
|
|
414
353
|
proc.exitCode = outcome.exitCode;
|
|
415
354
|
proc.signal = outcome.signal;
|
|
416
355
|
this.onProcessDone(proc, collected.stderr.readFrom(0).text, false);
|
|
356
|
+
disarm();
|
|
417
357
|
}, (error) => {
|
|
358
|
+
if (running !== void 0 && (proc.status === "killed" || spawnSignal?.aborted === true)) {
|
|
359
|
+
proc.status = "killed";
|
|
360
|
+
this.onProcessDone(proc, collected.stderr.readFrom(0).text, false);
|
|
361
|
+
disarm();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
418
364
|
proc.status = "killed";
|
|
419
365
|
let detail = "unprintable provider failure";
|
|
420
366
|
try {
|
|
421
367
|
detail = String(error);
|
|
422
368
|
} catch {}
|
|
423
|
-
|
|
424
|
-
|
|
369
|
+
providerFailure = {
|
|
370
|
+
error,
|
|
371
|
+
note: `subprocess failed before reporting an outcome: ${detail}`
|
|
372
|
+
};
|
|
373
|
+
this.onProcessDone(proc, providerFailure.note, true, error);
|
|
374
|
+
disarm();
|
|
425
375
|
}),
|
|
426
376
|
readOutput: () => {
|
|
427
377
|
const out = collected.stdout.readFrom(stdoutOffset);
|
|
@@ -442,10 +392,25 @@ var PwshLocalExecutor = class PwshLocalExecutor extends ShellExecutor {
|
|
|
442
392
|
kill: () => {
|
|
443
393
|
if (proc.status !== "running") return false;
|
|
444
394
|
proc.status = "killed";
|
|
445
|
-
running
|
|
395
|
+
running?.terminate();
|
|
446
396
|
return true;
|
|
397
|
+
},
|
|
398
|
+
result: () => {
|
|
399
|
+
resultPromise ??= proc.done.then(() => {
|
|
400
|
+
if (providerFailure !== void 0) throw providerFailure.error;
|
|
401
|
+
return {
|
|
402
|
+
exitCode: proc.exitCode,
|
|
403
|
+
signal: proc.signal,
|
|
404
|
+
...classify(),
|
|
405
|
+
timeoutMs: spec.timeoutMs,
|
|
406
|
+
stdout: finalOutput(collected.stdout),
|
|
407
|
+
stderr: finalOutput(collected.stderr)
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
return resultPromise;
|
|
447
411
|
}
|
|
448
412
|
};
|
|
413
|
+
if (!preparationTimedOut) onStarted?.(proc);
|
|
449
414
|
return proc;
|
|
450
415
|
}
|
|
451
416
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module @deepseek-ai/dsh-pwsh-local
|
|
14
14
|
*/
|
|
15
|
+
import type { Volatile } from '@deepseek-ai/cordis';
|
|
15
16
|
import { Context } from '@deepseek-ai/cordis';
|
|
16
17
|
import z from '@deepseek-ai/schemastery';
|
|
17
18
|
import { ShellExecutor } from '@deepseek-ai/dsh-shell';
|
|
18
|
-
import type { ShellExecRequest, ShellExecSpec,
|
|
19
|
+
import type { ShellExecRequest, ShellExecSpec, ShellExecution, ShellProcess } from '@deepseek-ai/dsh-shell';
|
|
19
20
|
/**
|
|
20
21
|
* Model-friendly environment overrides for PowerShell: disable colors and
|
|
21
22
|
* pagers that would garble tool output. `TERM=dumb` is a POSIX concept and is
|
|
@@ -35,37 +36,34 @@ export declare const ENV_OVERRIDES: {
|
|
|
35
36
|
* numbers stay accurate.
|
|
36
37
|
*/
|
|
37
38
|
export declare const ENCODING_PREAMBLE = "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [System.Text.UTF8Encoding]::new($false); ";
|
|
38
|
-
/**
|
|
39
|
+
/** Validated plugin configuration with live command budgets. */
|
|
39
40
|
export interface Config {
|
|
40
41
|
/** Default working directory for commands (default: process.cwd()). */
|
|
41
|
-
cwd
|
|
42
|
+
cwd: Volatile<string | undefined>;
|
|
42
43
|
/** Default foreground timeout in milliseconds. */
|
|
43
|
-
timeoutMs
|
|
44
|
+
timeoutMs: Volatile<number>;
|
|
44
45
|
/** Upper bound for per-call timeout overrides. */
|
|
45
|
-
maxTimeoutMs
|
|
46
|
+
maxTimeoutMs: Volatile<number>;
|
|
46
47
|
/** Per-stream in-memory output cap; overflow spills to a temp file. */
|
|
47
|
-
maxOutputBytes
|
|
48
|
+
maxOutputBytes: Volatile<number>;
|
|
48
49
|
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
|
49
|
-
maxSpillBytes
|
|
50
|
+
maxSpillBytes: Volatile<number>;
|
|
50
51
|
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
|
51
|
-
graceMs
|
|
52
|
+
graceMs: Volatile<number>;
|
|
52
53
|
/**
|
|
53
54
|
* Explicit pwsh executable. When omitted, well-known Windows install
|
|
54
55
|
* locations and PATH entries are probed in order (PowerShell 7 install,
|
|
55
56
|
* PATH entries such as the Microsoft Store install, then Windows
|
|
56
57
|
* PowerShell 5.1), falling back to a bare `pwsh` resolved through PATH.
|
|
57
58
|
*/
|
|
58
|
-
pwshPath
|
|
59
|
+
pwshPath: Volatile<string | undefined>;
|
|
59
60
|
}
|
|
60
|
-
/** The shape after schemastery applied the defaults (cwd/pwshPath have none). */
|
|
61
|
-
type ResolvedConfig = Required<Omit<Config, 'cwd' | 'pwshPath'>> & Pick<Config, 'cwd' | 'pwshPath'>;
|
|
62
61
|
export { candidatePwshPaths, resolvePwshPath } from './resolve.ts';
|
|
63
62
|
/**
|
|
64
63
|
* Reject a resolved section this executor could not run with. The schema
|
|
65
64
|
* expresses neither "positive and finite" nor the timer bound `graceMs` has to
|
|
66
|
-
* fit, so a stored value
|
|
67
|
-
* the
|
|
68
|
-
* @param config - the resolved section, schema-valid by construction.
|
|
65
|
+
* fit, so a stored value that cannot be used fails at the next command.
|
|
66
|
+
* @param config - the live configuration, schema-valid by construction.
|
|
69
67
|
* @throws Error naming the field that cannot be used.
|
|
70
68
|
*/
|
|
71
69
|
export declare function assertServiceablePwshConfig(config: Config): void;
|
|
@@ -75,17 +73,30 @@ export declare function assertServiceablePwshConfig(config: Config): void;
|
|
|
75
73
|
* this executor supplies their configured budgets per spawn.
|
|
76
74
|
*/
|
|
77
75
|
export declare class PwshLocalExecutor extends ShellExecutor {
|
|
76
|
+
readonly config: Config;
|
|
78
77
|
static inject: string[];
|
|
79
|
-
static Config: z<
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
static Config: z<Schemastery.ObjectS<NoInfer<{
|
|
79
|
+
cwd: z<string, string, "volatile">;
|
|
80
|
+
timeoutMs: z<number, number, "volatile-defined">;
|
|
81
|
+
maxTimeoutMs: z<number, number, "volatile-defined">;
|
|
82
|
+
maxOutputBytes: z<number, number, "volatile-defined">;
|
|
83
|
+
maxSpillBytes: z<number, number, "volatile-defined">;
|
|
84
|
+
graceMs: z<number, number, "volatile-defined">;
|
|
85
|
+
pwshPath: z<string, string, "volatile">;
|
|
86
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
87
|
+
cwd: z<string, string, "volatile">;
|
|
88
|
+
timeoutMs: z<number, number, "volatile-defined">;
|
|
89
|
+
maxTimeoutMs: z<number, number, "volatile-defined">;
|
|
90
|
+
maxOutputBytes: z<number, number, "volatile-defined">;
|
|
91
|
+
maxSpillBytes: z<number, number, "volatile-defined">;
|
|
92
|
+
graceMs: z<number, number, "volatile-defined">;
|
|
93
|
+
pwshPath: z<string, string, "volatile">;
|
|
94
|
+
}>>, "plain">;
|
|
82
95
|
/** The declared executable the current {@link pwshPath} was resolved from. */
|
|
83
96
|
private declaredPwshPath;
|
|
84
97
|
/** The pwsh executable resolved from the current config. */
|
|
85
98
|
private resolvedPwshPath;
|
|
86
|
-
/**
|
|
87
|
-
get config(): ResolvedConfig;
|
|
88
|
-
/** The pwsh executable every command runs through. */
|
|
99
|
+
/** The pwsh executable every command runs through; a changed declared path is probed again on the next read. */
|
|
89
100
|
get pwshPath(): string;
|
|
90
101
|
constructor(ctx: Context, config: Config);
|
|
91
102
|
/**
|
|
@@ -97,7 +108,7 @@ export declare class PwshLocalExecutor extends ShellExecutor {
|
|
|
97
108
|
/**
|
|
98
109
|
* The pwsh invocation argv for one resolved spec — the argv-level seam a
|
|
99
110
|
* confining subclass wraps through `ctx.sandbox.confine` (the pwsh twin of
|
|
100
|
-
* `dsh-bash-local`'s `
|
|
111
|
+
* `dsh-bash-local`'s `executeArgv` hook; see
|
|
101
112
|
* `@deepseek-ai/dsh-pwsh-sandbox`).
|
|
102
113
|
*/
|
|
103
114
|
protected argv(spec: ShellExecSpec): string[];
|
|
@@ -105,20 +116,18 @@ export declare class PwshLocalExecutor extends ShellExecutor {
|
|
|
105
116
|
private spawnSpec;
|
|
106
117
|
/** The collect-mode readers the executor itself requested (present by construction). */
|
|
107
118
|
private static collected;
|
|
108
|
-
|
|
119
|
+
execute(spec: ShellExecSpec): Promise<ShellExecution>;
|
|
109
120
|
/**
|
|
110
|
-
*
|
|
121
|
+
* Execute an explicit argv with the lifecycle, environment, output,
|
|
122
|
+
* deadline, and cancellation semantics of this executor. Subclasses use this
|
|
123
|
+
* after replacing the public command's shell argv at an execution boundary.
|
|
111
124
|
* @param spec - resolved execution settings and caller-owned command metadata.
|
|
112
|
-
* @param argvOrPrepare - exact argv, or preparation
|
|
113
|
-
* @
|
|
125
|
+
* @param argvOrPrepare - exact argv, or preparation using the execution cancellation signal.
|
|
126
|
+
* @param onStarted - installs provider facts synchronously before the handle can settle.
|
|
127
|
+
* @returns the live execution handle; spawn rejection settles the handle as
|
|
128
|
+
* killed while `result()` carries the same failure as its rejection.
|
|
114
129
|
*/
|
|
115
|
-
protected
|
|
116
|
-
result: ShellRunResult;
|
|
117
|
-
spawnRequested: boolean;
|
|
118
|
-
}>;
|
|
119
|
-
start(spec: ShellExecSpec): Promise<ShellProcess>;
|
|
120
|
-
/** Background start of an exact argv (the confining subclass re-wraps it). */
|
|
121
|
-
protected startArgv(spec: ShellExecSpec, argv: readonly string[]): ShellProcess;
|
|
130
|
+
protected executeArgv(spec: ShellExecSpec, argvOrPrepare: readonly string[] | ((signal: AbortSignal) => Promise<readonly string[]>), onStarted?: (process: ShellExecution) => void): Promise<ShellExecution>;
|
|
122
131
|
/**
|
|
123
132
|
* Settlement hook for subclasses that attach execution facts to a process.
|
|
124
133
|
* The base implementation is intentionally empty. Mirrored from
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-pwsh-local",
|
|
3
3
|
"description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,21 +27,19 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@deepseek-ai/dsh-
|
|
31
|
-
"@deepseek-ai/
|
|
32
|
-
"@deepseek-ai/dsh-
|
|
33
|
-
"@deepseek-ai/
|
|
34
|
-
"@deepseek-ai/dsh-settings": "^0.1.6-alpha.2"
|
|
30
|
+
"@deepseek-ai/dsh-shell": "0.1.7-alpha.2",
|
|
31
|
+
"@deepseek-ai/dsh-timeout": "0.1.7-alpha.2",
|
|
32
|
+
"@deepseek-ai/dsh-subprocess": "0.1.7-alpha.2",
|
|
33
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
35
34
|
},
|
|
36
35
|
"dependencies": {
|
|
37
|
-
"@deepseek-ai/schemastery": "
|
|
36
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
"@deepseek-ai/dsh-shell": "
|
|
41
|
-
"@deepseek-ai/dsh-subprocess": "
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/cordis": "
|
|
45
|
-
"@deepseek-ai/dsh-settings": "^0.1.6-alpha.2"
|
|
39
|
+
"@deepseek-ai/dsh-shell": "0.1.7-alpha.2",
|
|
40
|
+
"@deepseek-ai/dsh-subprocess": "0.1.7-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-timeout": "0.1.7-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.7-alpha.2",
|
|
43
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
46
44
|
}
|
|
47
45
|
}
|