@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 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: 1dbc90835cb95a0e171a33b10c7409a181dd98cf
6
- README.zh.md: f87c7d435cccb600d123cf80264e027657ef1835
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 with `run` and read its output from the result; 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.
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 result = await ctx.shell.run(ctx.shell.resolve({ command: 'Get-ChildItem' }))
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
- Await `start` to run a command in the background; it resolves with the prepared process handle and no execution timeout applies. 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
+ 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
- When a settings provider is composed, this executor registers the capability's shared `shell` settings namespace — the same one the POSIX family uses, because a host composes exactly one provider of `ctx.shell` — so a user section in `settings.yaml` layers over the composition entry and the next command runs with the new budgets. Values the schema cannot judge — positive and finite numbers, and the `graceMs` timer bound — are refused at the write, leaving the running executor on its last good section.
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 single-delivery** — `SubprocessHandle.done` can reject before or after target execution begins, so the executor injects the stage-neutral `subprocess failed before reporting an outcome: …` into exactly one `readOutput()` delta; a reader that discards that delta cannot recover it.
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
- 用 `run` 运行命令并从结果读取输出;非零退出、超时或取消都会返回描述性结果,只有基础设施失败才会导致调用被拒绝。命令字符串作为单个参数传给 `-Command`:由 PowerShell 自己解析文本,不存在中间 shell,因此没有需要转义的 shell 引号层,原生 Win32 路径也原样通过。每条命令都先固定 UTF-8 输出,因此即使在 Windows PowerShell 5.1 兜底上,非 ASCII 输出也不会乱码。环境默认面向模型:`NO_COLOR=1 PAGER=cat GIT_PAGER=cat`(没有 `TERM=dumb`——那是 POSIX 概念),调用方显式提供的条目仍然优先。
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 result = await ctx.shell.run(ctx.shell.resolve({ command: 'Get-ChildItem' }))
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
- 等待 `start` 即可在后台运行命令;它完成准备后返回进程句柄,且不应用执行超时。取消或准备失败会在发布句柄前拒绝调用。`readOutput()` 把流增量合并为一次消费式读取,并在 `[stderr]` 分段下标记 stderr;`kill()` 终止由提供方管理的 range;`done` 在 direct command 关闭时结算且绝不 reject。job id、所有权、轮询与通知属于通用 `ctx.jobs` 运行时,工具层会把句柄注册进去。
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
- 当组合了设置提供方时,本执行器注册该能力共享的 `shell` 设置命名空间——与 POSIX 家族共用同一个,因为一个宿主只组装一个 `ctx.shell` 提供方——因此 `settings.yaml` 中的用户段会叠加在组合条目之上,下一条命令即按新预算运行。schema 无法判定的值——正有限数字与 `graceMs` 的定时器上界——会在写入时被拒绝,运行中的执行器保持它最后一份可用的段。
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
- - **后台提供方失败提示只投递一次**——`SubprocessHandle.done` 可能在目标开始执行前或后被拒绝,因此执行器会将不指明失败阶段的 `subprocess failed before reporting an outcome: …` 注入且仅注入一个 `readOutput()` 增量;丢弃该增量的读取方无法恢复它。
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 { SHELL_SETTINGS_NAMESPACE, ShellExecutor } from "@deepseek-ai/dsh-shell";
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 is refused where it is written instead of failing at
179
- * the next command.
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
- const resolved = config;
185
- assertPositiveFinite("timeoutMs", resolved.timeoutMs);
186
- assertPositiveFinite("maxTimeoutMs", resolved.maxTimeoutMs);
187
- assertPositiveFinite("maxOutputBytes", resolved.maxOutputBytes);
188
- assertPositiveFinite("maxSpillBytes", resolved.maxSpillBytes);
189
- assertPositiveFinite("graceMs", resolved.graceMs);
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
- /** Validated config (schemastery applied the defaults before construction). */
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
- const entry = config;
225
- assertServiceablePwshConfig(entry);
226
- this.source = () => entry;
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
- const timeoutMs = clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, "pwsh-local: request.timeoutMs");
251
- const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes;
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 `runArgv`/`startArgv` hooks; see
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 run(spec) {
316
- return (await this.runArgv(spec, this.argv(spec))).result;
241
+ async execute(spec) {
242
+ return this.executeArgv(spec, this.argv(spec));
317
243
  }
318
244
  /**
319
- * Run argv preparation and execution under one foreground deadline.
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 cancelled by the same deadline as execution.
322
- * @returns the foreground result and whether argv reached the subprocess provider.
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 runArgv(spec, argvOrPrepare) {
325
- const env_1 = {
326
- stack: [],
327
- error: void 0,
328
- hasError: false
329
- };
330
- try {
331
- const d = __addDisposableResource(env_1, deadline(spec.signal, spec.timeoutMs, "BASH_TIMEOUT"), false);
332
- let argv;
333
- if (typeof argvOrPrepare === "function") {
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
- timeoutMs: spec.timeoutMs,
381
- stdout: finalOutput(collected.stdout),
382
- stderr: finalOutput(collected.stderr)
383
- }
265
+ aborted: d.signal.aborted && !timedOut
266
+ };
384
267
  };
385
- } catch (e_1) {
386
- env_1.error = e_1;
387
- env_1.hasError = true;
388
- } finally {
389
- __disposeResources(env_1);
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
- async start(spec) {
393
- return Promise.resolve(this.startArgv(spec, this.argv(spec)));
394
- }
395
- /** Background start of an exact argv (the confining subclass re-wraps it). */
396
- startArgv(spec, argv) {
397
- spec.signal?.throwIfAborted();
398
- const running = this.ctx.subprocess.spawn(this.spawnSpec(spec, this.config.maxOutputBytes, spec.signal, argv));
399
- const collected = PwshLocalExecutor.collected(running);
400
- let providerFailureNote;
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
- const note = providerFailureNote ?? "";
403
- providerFailureNote = void 0;
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
- done: running.done.then((outcome) => {
413
- if (proc.status === "running") proc.status = spec.signal?.aborted === true || outcome.signal !== null ? "killed" : "completed";
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
- providerFailureNote = `subprocess failed before reporting an outcome: ${detail}`;
424
- this.onProcessDone(proc, providerFailureNote, true, error);
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.terminate();
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
  /**
@@ -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, ShellProcess, ShellRunResult } from '@deepseek-ai/dsh-shell';
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
- /** Plugin config (all optional — `static Config` supplies the defaults). */
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?: string;
42
+ cwd: Volatile<string | undefined>;
42
43
  /** Default foreground timeout in milliseconds. */
43
- timeoutMs?: number;
44
+ timeoutMs: Volatile<number>;
44
45
  /** Upper bound for per-call timeout overrides. */
45
- maxTimeoutMs?: number;
46
+ maxTimeoutMs: Volatile<number>;
46
47
  /** Per-stream in-memory output cap; overflow spills to a temp file. */
47
- maxOutputBytes?: number;
48
+ maxOutputBytes: Volatile<number>;
48
49
  /** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
49
- maxSpillBytes?: number;
50
+ maxSpillBytes: Volatile<number>;
50
51
  /** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
51
- graceMs?: number;
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?: string;
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 is refused where it is written instead of failing at
67
- * the next command.
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<Config>;
80
- /** The currently authoritative config: the settings section, or the composition entry. */
81
- private source;
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
- /** Validated config (schemastery applied the defaults before construction). */
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 `runArgv`/`startArgv` hooks; see
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
- run(spec: ShellExecSpec): Promise<ShellRunResult>;
119
+ execute(spec: ShellExecSpec): Promise<ShellExecution>;
109
120
  /**
110
- * Run argv preparation and execution under one foreground deadline.
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 cancelled by the same deadline as execution.
113
- * @returns the foreground result and whether argv reached the subprocess provider.
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 runArgv(spec: ShellExecSpec, argvOrPrepare: readonly string[] | ((signal: AbortSignal) => Promise<readonly string[]>)): Promise<{
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.6-alpha.2",
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-subprocess": "^0.1.6-alpha.2",
31
- "@deepseek-ai/cordis": "^4.0.2",
32
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
33
- "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
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": "^3.18.2"
36
+ "@deepseek-ai/schemastery": "~3.18.4"
38
37
  },
39
38
  "devDependencies": {
40
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
41
- "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-subprocess-local": "^0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
44
- "@deepseek-ai/cordis": "^4.0.2",
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
  }