refactor(runtime): compose consumers over fs and subprocess
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess/README.md
|
||||
README.md: e59dd96df036826f36bd0286c977438d2d87d1cf
|
||||
README.zh.md: e8fb89dfd1f8c41a0caefc96469c13d9ae7d415d
|
||||
README.md: 84b4b0c11c74c96929fa97b58fb33156d44e6ef1
|
||||
README.zh.md: dbd80a1c975719884481501f5cc43798e464a4fb
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The subprocess seam (`ctx.subprocess`). The abstract `SubprocessService` exposes one method — `spawn(spec): SubprocessHandle` — plus the vocabulary shared by every consumer: the fully-explicit `SubprocessSpawnSpec`, `SubprocessHandle` with its non-consuming offset-based output readers, `SubprocessOutcome`, `CollectedOutput`, and the managed `DSH_*` environment namespace (`DSH_ENV_PREFIX`, `DshEnvironment`). The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md).
|
||||
The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessService` exposes its canonical `cwd`, private `runtimeRoot`, executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, tree/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md).
|
||||
|
||||
## Contract
|
||||
|
||||
- `spawn(spec)` returns immediately with a live handle; `done` resolves at process close with exit facts (`SubprocessOutcome` carries no output and no cause classification) and rejects only for spawn-level failures.
|
||||
- The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the calling seam's config, not to a hidden subprocess-service default (the `dsh-bash` request/spec split is the owning template). Grace must be positive, finite, and no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), so the implementation can represent it with one Node timer instead of accepting a value that Node collapses to one millisecond. `argv` is never shell-interpreted; a consumer that wants a shell passes `['bash', '-c', command]` itself.
|
||||
- `cwd` and `runtimeRoot` are absolute paths in the provider's execution world. Consumers materialize private helpers below `runtimeRoot`, never in a host-only temp directory. `resolveExecutable(command, env?, signal?)` verifies absolute commands or resolves bare names against that world's scrubbed PATH plus explicit overrides.
|
||||
- The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the calling seam's config, not to a hidden subprocess-service default (the `dsh-bash` request/spec split is the owning template). `argv` is never shell-interpreted; a consumer that wants a shell passes `['bash', '-c', command]` itself.
|
||||
- Stdio is Node-shaped per stream: `'pipe'` hands the caller the raw stream for its own protocol framing (LSP JSON-RPC, ACP ndjson), `'inherit'` passes the parent descriptor through for diagnostics, and collect mode (`{ maxBytes, spill? }`) buffers a bounded tail with an optional full-stream spill file. Collect readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; a read whose offset slid out of the in-memory tail is `lossy` and points at the spill file when one exists. Collected output stays readable after settlement.
|
||||
- Termination is tree-scoped on every platform (POSIX detached groups with direct-child fallback; Windows `taskkill /T`): `terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL (idempotent, driven by the spec's abort signal too, a no-op once the tree is gone), and `waitForExit(signal?)` observes whole-tree liveness so a consumer-owned teardown ladder holds each tier on real quiescence — the manager reacts but never classifies why (callers own deadlines, teardown ladders, and cause classification).
|
||||
- `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` are the one shared scrub definition: ambient credential-shaped and `DSH_*` names are dropped, and the spec's explicit `env` merges after the scrub with no namespace validation — a string deliberately forwards or overrides a value, while an `undefined` tombstone removes an ordinary ambient entry. Spawners that cannot route through the service (node-pty backends, SDK-managed transports) import the scrub.
|
||||
- `spawnTerminal(spec)` is the only non-pipe primitive. Its handle owns a real PTY, valid-UTF-8 byte I/O, foreground-process-group inspection/signalling, TERM-to-KILL whole-session cleanup, and a quiescence wait. The output stream ends after queued output when the top-level process exits; a live transport failure rejects `done`. These operations remain one substrate primitive because ordinary pipes cannot allocate a controlling terminal or prove and clean the complete terminal session; readiness, scrollback, and owner policy remain in the PTY consumer.
|
||||
- `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` are the one shared scrub definition: ambient credential-shaped and `DSH_*` names are dropped, and explicit `env` merges after the scrub. The local ordinary and terminal spawns both apply it; SDK-managed transports that own their spawn may import it directly.
|
||||
- Disposal of the service terminates all still-running managed processes and awaits their exit.
|
||||
|
||||
See the [subprocess data-structure catalog](../../../docs/core-data-structures/subprocess.md) and the [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md).
|
||||
@@ -25,5 +27,5 @@ No direct invalidation; the named consumers own any request-prefix changes.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **node-pty and SDK-managed spawns share only the scrub** — the PTY backend's terminal fork and the MCP SDK's own stdio transport cannot route their spawns through this seam (the library owns the fork/spawn call); they import `scrubbedParentEnv` so the environment policy stays single-sourced.
|
||||
- **SDK-managed spawns remain outside** — an SDK transport that owns its internal spawn cannot route that call through this service; it can still import `scrubbedParentEnv` so environment policy stays single-sourced.
|
||||
- **Teardown ladders are consumer-owned** — the seam ships signalling verbs and the tree-liveness wait, not a canned quiesce sequence; each out-of-process consumer encodes its child's cooperation shape itself (the ACP backend's stdin-EOF-first ladder is the in-repo template).
|
||||
|
||||
@@ -2,28 +2,30 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
子进程 seam(`ctx.subprocess`)。抽象的 `SubprocessService` 只暴露一个方法:`spawn(spec): SubprocessHandle`,外加所有消费方共享的词汇:完全显式的 `SubprocessSpawnSpec`、携带基于偏移量的非消费式输出读取器的 `SubprocessHandle`、`SubprocessOutcome`、`CollectedOutput`,以及受管的 `DSH_*` 环境命名空间(`DSH_ENV_PREFIX`、`DshEnvironment`)。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。
|
||||
进程管理器 seam(`ctx.subprocess`)是同一执行世界中的进程侧。抽象的 `SubprocessService` 公开其规范化 `cwd`、私有 `runtimeRoot`、可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。
|
||||
|
||||
## 契约
|
||||
|
||||
- `spawn(spec)` 立即返回一个活动句柄;`done` 在进程关闭时以退出事实 resolve(`SubprocessOutcome` 不携带输出,也不携带原因分类),仅在 spawn 层面失败时 reject。
|
||||
- spec 完全显式(argv、cwd、按流划分的 stdio 处置方式(disposition)、宽限期),因为随部署变化的默认值属于调用方 seam 的配置,而不属于某个隐藏的子进程默认值(`dsh-bash` 的 request/spec 拆分是这条规则的所属模板)。宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样实现便可用一个 Node 定时器表示它,而不会接受会被 Node 折叠为 1 毫秒的值。`argv` 绝不经过 shell 解释;需要 shell 的消费方自行传入 `['bash', '-c', command]`。
|
||||
- stdio 按流采用 Node 风格:`'pipe'` 把原始流交给调用方做自己的协议分帧(LSP 的 JSON-RPC、ACP(Agent Client Protocol)的 ndjson),`'inherit'` 直通父进程描述符以承载诊断输出,收集模式(collect)`{ maxBytes, spill? }` 则缓冲一段有界尾部,外加可选的完整流 spill 文件。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;偏移量滑出内存尾部窗口的读取标记为 `lossy`,并在 spill 文件存在时指向它。收集到的输出在结算后仍可读取。
|
||||
- `spawn(spec)` 立即返回一个实时句柄;`done` 在进程关闭时以退出事实 resolve(`SubprocessOutcome` 不携带输出,也不携带原因分类),仅在 spawn 层面失败时 reject。
|
||||
- `cwd` 与 `runtimeRoot` 是提供方执行世界中的绝对路径。消费方在 `runtimeRoot` 下物化私有辅助程序,绝不使用仅宿主可见的临时目录。`resolveExecutable(command, env?, signal?)` 验证绝对命令,或根据该执行世界清理后的 PATH 加显式覆盖来解析裸名称。
|
||||
- spec 完全显式(argv、cwd、按流划分的 stdio 处置方式(disposition)、宽限期),因为随部署变化的默认值属于调用方 seam 的配置,而不属于某个隐藏的进程管理器默认值(`dsh-bash` 的 request/spec 拆分是这条规则的所属模板)。`argv` 绝不经过 shell 解释;需要 shell 的消费方自行传入 `['bash', '-c', command]`。
|
||||
- stdio 按流采用 Node 形状:`'pipe'` 把原始流交给调用方做自己的协议分帧(LSP 的 JSON-RPC、ACP(Agent Client Protocol)的 ndjson),`'inherit'` 直通父进程描述符以承载诊断输出,收集模式(collect)`{ maxBytes, spill? }` 则缓冲一段有界尾部,外加可选的完整流 spill 文件。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;偏移量滑出内存尾部窗口的读取标记为 `lossy`,并在 spill 文件存在时指向它。收集到的输出在结算后仍可读取。
|
||||
- 终止在每个平台上都以进程树为范围(POSIX 用 detached 进程组并以直接子进程回退;Windows 用 `taskkill /T`):`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级(幂等,也由 spec 的 abort 信号驱动,进程树消亡后为空操作);`waitForExit(signal?)` 观察整棵进程树的存活状态,使消费方自有的拆卸阶梯能在真正完全停稳后才进入下一层。管理器只响应中止,但绝不判定原因(deadline、拆卸阶梯与原因分类归调用方所有)。
|
||||
- `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` 是唯一一份共享的环境清理定义:环境中形似凭据的名称与 `DSH_*` 名称都会被丢弃,spec 的显式 `env` 在清理后合并且不做命名空间校验——字符串会有意转发或覆盖某个值,而 `undefined` tombstone 则会删除普通的环境条目。无法把 spawn 路由到该服务的进程启动方(node-pty 后端、由 SDK 管理的传输层)会导入该环境清理定义。
|
||||
- `spawnTerminal(spec)` 是唯一的非管道原语。其句柄负责真实 PTY、有效 UTF-8 字节 I/O、前台进程组检查/信号发送、TERM→KILL 全会话清理,以及等待完全停稳。顶层进程退出后,输出流会在排完队列中的输出后结束;存活期间的传输故障会拒绝 `done`。这些操作仍属于一项基底原语,因为普通管道无法分配控制终端,也无法证明并清理完整的终端会话;就绪检测、scrollback 与所有者策略仍归 PTY 消费方所有。
|
||||
- `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` 是唯一一份共享的凭据清除定义:环境中形似凭据的名称与 `DSH_*` 名称都会被丢弃,显式 `env` 在清除之后合并。本地普通 spawn 与终端 spawn 都应用这一定义;自行拥有 spawn 的 SDK 管理传输层可以直接导入它。
|
||||
- 服务自身的 dispose(资源释放)会终止所有仍在运行的受管进程并等待其退出。
|
||||
|
||||
参见[子进程数据结构目录](../../../docs/core-data-structures/subprocess.md)与[seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
|
||||
参见[进程管理器数据结构目录](../../../docs/core-data-structures/subprocess.md)与 [seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
通过消费方 seam 间接影响(目前是 `dsh-tool-bash` 背后的 bash 执行器家族);进程输出和生命周期的全部面向模型渲染均由消费方负责。
|
||||
通过消费方 seam 间接影响(目前是 `dsh-tool-bash` 背后的 bash 执行器家族);进程输出与生命周期面向模型的全部渲染归消费方所有。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
|
||||
不会直接失效;请求前缀变更由具名消费方负责。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **node-pty 与由 SDK 管理的 spawn 只共享环境清理**:PTY 后端的终端 fork 与 MCP SDK 自己的 stdio 传输层无法把 spawn 路由到这道 seam(fork/spawn 调用归库所有);它们改为导入 `scrubbedParentEnv`,使环境策略保持单一来源。
|
||||
- **拆卸阶梯归消费方所有**:该 seam 只提供信号动词与进程树存活等待,不提供现成的停稳序列;每个进程外消费方自行编码其子进程的配合方式(ACP 后端以 stdin EOF 打头的阶梯是仓库内模板)。
|
||||
- **由 SDK 管理的 spawn 仍在服务之外**:自行拥有内部 spawn 的 SDK 传输层无法经该服务路由这次调用;它仍可导入 `scrubbedParentEnv`,使环境策略保持单一来源。
|
||||
- **拆卸阶梯归消费方所有**:该 seam 只提供信号动词与进程树存活等待,不提供现成的停稳序列;每个进程外消费方自行编码其子进程的配合形状(ACP 后端以 stdin EOF 打头的阶梯是仓库内模板)。
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* The subprocess seam (`ctx.subprocess`): spawn fully-specified commands into
|
||||
* managed process trees with Node-shaped stdio dispositions — raw pipes for
|
||||
* protocol streams, inherit for diagnostics, bounded spill-backed collection
|
||||
* for batch output — plus tree-scoped signalling. Command defaulting, shell
|
||||
* semantics, deadlines, teardown ladders, framing, and presentation belong to
|
||||
* consumers; the bash executor seam is the owning template. The local implementation lives in
|
||||
* The subprocess seam (`ctx.subprocess`): execution-world process coordinates,
|
||||
* executable lookup, fully specified managed process trees with raw or
|
||||
* collected stdio, and one terminal-process primitive. Command defaulting,
|
||||
* shell semantics, deadlines, protocol framing, terminal readiness, and
|
||||
* presentation belong to consumers. The local implementation lives in
|
||||
* `@deepseek-ai/dsh-subprocess-local`.
|
||||
* @module @deepseek-ai/dsh-subprocess
|
||||
*/
|
||||
@@ -12,6 +11,7 @@
|
||||
import { Context, Service } from 'cordis'
|
||||
import { DSH_ENV_PREFIX } from './types.ts'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts'
|
||||
import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from './types.ts'
|
||||
|
||||
export { DSH_ENV_PREFIX } from './types.ts'
|
||||
export type {
|
||||
@@ -28,6 +28,10 @@ export type {
|
||||
SubprocessSpawnSpec,
|
||||
SubprocessStdinMode,
|
||||
SubprocessStdio,
|
||||
SubprocessTerminalForeground,
|
||||
SubprocessTerminalHandle,
|
||||
SubprocessTerminalSignal,
|
||||
SubprocessTerminalSpawnSpec,
|
||||
} from './types.ts'
|
||||
|
||||
/**
|
||||
@@ -74,6 +78,8 @@ declare module 'cordis' {
|
||||
* duplicate-service behavior).
|
||||
*
|
||||
* Implementations must honor these semantics:
|
||||
* - {@link cwd}, {@link runtimeRoot}, and executable paths belong to one
|
||||
* execution world shared with the mounted filesystem provider.
|
||||
* - {@link spawn} returns immediately with a live handle; `done` resolves at
|
||||
* process close with exit facts and rejects only for spawn-level failures.
|
||||
* - Collect-mode readers are offset-based and non-consuming, so independent
|
||||
@@ -87,12 +93,37 @@ declare module 'cordis' {
|
||||
* quiescence.
|
||||
* - Disposal of the service terminates all still-running managed processes
|
||||
* and awaits their exit.
|
||||
* - {@link spawnTerminal} owns terminal allocation, byte transport,
|
||||
* foreground groups, signalling, and whole-session quiescence; readiness
|
||||
* and persistent-shell policy stay in the PTY consumer. Its output stream
|
||||
* ends after queued terminal output when the top-level process exits.
|
||||
*/
|
||||
export abstract class SubprocessService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'subprocess')
|
||||
}
|
||||
|
||||
/** Canonical default cwd in this provider's execution world. */
|
||||
abstract readonly cwd: string
|
||||
|
||||
/** Private directory for runtime artifacts in this provider's execution world. */
|
||||
abstract readonly runtimeRoot: string
|
||||
|
||||
/**
|
||||
* Resolve one configured executable in this provider's execution world.
|
||||
* Absolute paths are verified; bare names use the provider's scrubbed PATH
|
||||
* plus explicit environment overrides.
|
||||
* @param command - absolute executable path or bare PATH name.
|
||||
* @param env - explicit environment entries used for lookup.
|
||||
* @param signal - aborts remote or local lookup.
|
||||
* @returns a canonical executable path.
|
||||
*/
|
||||
abstract resolveExecutable(
|
||||
command: string,
|
||||
env?: Readonly<Record<string, string>>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string>
|
||||
|
||||
/**
|
||||
* Start one managed child process from a fully-specified spec; this seam
|
||||
* applies no defaults.
|
||||
@@ -100,6 +131,15 @@ export abstract class SubprocessService extends Service {
|
||||
* @returns the live process handle (streams/readers, signalling, outcome promise).
|
||||
*/
|
||||
abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
|
||||
|
||||
/**
|
||||
* Allocate a real terminal and start one owned process session. This is the
|
||||
* only non-pipe process primitive: implementations own terminal byte I/O,
|
||||
* foreground groups, signals, and complete session-tree cleanup.
|
||||
* @param spec - fully specified argv, cwd, environment, dimensions, grace, and cancellation.
|
||||
* @returns the live terminal handle after allocation succeeds.
|
||||
*/
|
||||
abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
|
||||
}
|
||||
|
||||
export default SubprocessService
|
||||
|
||||
@@ -192,3 +192,71 @@ export interface SubprocessHandle {
|
||||
*/
|
||||
waitForExit(signal?: AbortSignal): Promise<boolean>
|
||||
}
|
||||
|
||||
/** Signals supported by the terminal-process primitive. */
|
||||
export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP'
|
||||
|
||||
/** A fully specified terminal-process spawn. */
|
||||
export interface SubprocessTerminalSpawnSpec {
|
||||
/** Executable and arguments; `argv[0]` is the program. */
|
||||
argv: readonly string[]
|
||||
/** Working directory in this subprocess provider's execution world. */
|
||||
cwd: string
|
||||
/** Explicit environment layered after the provider's ambient scrub. */
|
||||
env?: Record<string, string> | undefined
|
||||
/** Initial terminal row count. */
|
||||
rows: number
|
||||
/** Initial terminal column count. */
|
||||
cols: number
|
||||
/** TERM-to-KILL cleanup grace for the complete terminal session. */
|
||||
graceMs: number
|
||||
/** Cancellation of setup or the live terminal session. */
|
||||
signal?: AbortSignal | undefined
|
||||
}
|
||||
|
||||
/** Current foreground process-group facts for one terminal. */
|
||||
export interface SubprocessTerminalForeground {
|
||||
/** Foreground process-group id published by the terminal driver. */
|
||||
processGroupId: number
|
||||
/** Whether the provider can currently prove that group is waiting on terminal input. */
|
||||
inputWaiting: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* One live terminal process and its owned OS session. Terminal allocation,
|
||||
* foreground-group inspection/signalling, and session-tree cleanup are one
|
||||
* deep subprocess primitive because none can be reconstructed from ordinary
|
||||
* piped stdio without substrate-specific process control.
|
||||
*/
|
||||
export interface SubprocessTerminalHandle {
|
||||
/** Top-level terminal process id. */
|
||||
readonly pid: number
|
||||
/** UTF-8 terminal output bytes in delivery order; ends after queued output when the terminal exits. */
|
||||
readonly output: Readable
|
||||
/** Resolves when the top-level process exits; rejects only for a live transport failure. */
|
||||
readonly done: Promise<SubprocessOutcome>
|
||||
/**
|
||||
* Write bytes to the terminal input.
|
||||
* @param data - valid UTF-8 bytes to deliver without implicit newline conversion.
|
||||
*/
|
||||
write(data: Uint8Array): Promise<void>
|
||||
/**
|
||||
* Inspect the current foreground process group.
|
||||
* @returns its id and input-wait fact, or undefined when no foreground group can be resolved.
|
||||
*/
|
||||
inspectForeground(): Promise<SubprocessTerminalForeground | undefined>
|
||||
/**
|
||||
* Deliver a signal to the current foreground process group.
|
||||
* @param signal - permitted terminal signal.
|
||||
* @returns the exact group id that received it.
|
||||
*/
|
||||
signalForeground(signal: SubprocessTerminalSignal): Promise<number>
|
||||
/** Begin idempotent TERM-to-KILL cleanup of the complete terminal session. */
|
||||
terminate(): void
|
||||
/**
|
||||
* Await whole-session quiescence, not only top-level process exit.
|
||||
* @param signal - optional bound for this wait.
|
||||
* @returns true after quiescence, false when `signal` aborts first.
|
||||
*/
|
||||
waitForExit(signal?: AbortSignal): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { Context } from 'cordis'
|
||||
import { scrubbedParentEnv, SubprocessService } from '@deepseek-ai/dsh-subprocess'
|
||||
import type { SubprocessHandle, SubprocessOutputRead, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import type {
|
||||
SubprocessHandle,
|
||||
SubprocessOutputRead,
|
||||
SubprocessSpawnSpec,
|
||||
SubprocessTerminalHandle,
|
||||
SubprocessTerminalSpawnSpec,
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
|
||||
/**
|
||||
* Minimal concrete service: a hand-built handle. The seam is spawn-only —
|
||||
@@ -9,6 +16,13 @@ import type { SubprocessHandle, SubprocessOutputRead, SubprocessSpawnSpec } from
|
||||
* is all an implementation owes the abstract class.
|
||||
*/
|
||||
class StubSubprocessService extends SubprocessService {
|
||||
readonly cwd = '/stub'
|
||||
readonly runtimeRoot = '/stub/.runtime'
|
||||
|
||||
async resolveExecutable(command: string): Promise<string> {
|
||||
return `/bin/${command}`
|
||||
}
|
||||
|
||||
spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
|
||||
const read: SubprocessOutputRead = { text: '', nextOffset: 0, lossy: false }
|
||||
const collected = spec.stdio.stdout !== 'pipe' && spec.stdio.stdout !== 'inherit'
|
||||
@@ -25,6 +39,19 @@ class StubSubprocessService extends SubprocessService {
|
||||
waitForExit: () => Promise.resolve(true),
|
||||
}
|
||||
}
|
||||
|
||||
async spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle> {
|
||||
return {
|
||||
pid: spec.argv.length,
|
||||
output: new PassThrough(),
|
||||
done: Promise.resolve({ exitCode: 0, signal: null }),
|
||||
write: async () => {},
|
||||
inspectForeground: async () => ({ processGroupId: 1, inputWaiting: true }),
|
||||
signalForeground: async () => 1,
|
||||
terminate: () => {},
|
||||
waitForExit: async () => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('SubprocessService seam', () => {
|
||||
|
||||
Reference in New Issue
Block a user