@deepseek-ai/dsh-subprocess-local 0.0.1-rc.1

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.
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-subprocess-local`.
4
+ * @module @deepseek-ai/dsh-subprocess-local/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-subprocess-local";
7
+ /** Cordis companion plugin name. */
8
+ const name = "subprocess-local-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this package exposes no independent event sequence or mutable data relation
13
+ * beyond contracts enforced at its owning seam.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Local Service provider for the subprocess capability seam. Each spawn is a detached
3
+ * process tree with the spec's per-stream stdio dispositions; disposal
4
+ * terminates and joins live trees. It has no config: every disposition and
5
+ * limit arrives on the spec, so the deployment-varying choices stay with the
6
+ * caller's config (the bash executor's, the LSP host's, …).
7
+ * @module @deepseek-ai/dsh-subprocess-local
8
+ */
9
+ import { Context } from '@deepseek-ai/cordis';
10
+ import { SubprocessService } from '@deepseek-ai/dsh-subprocess';
11
+ import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
12
+ import type { SpawnInternals } from './spawn.ts';
13
+ import type { ProcessInspector } from './process-inspector.ts';
14
+ /**
15
+ * Local subprocess service: detached process trees, Node-shaped stdio
16
+ * dispositions (raw pipes, inherit, bounded tail-keep collection with spill
17
+ * files), credential-scrubbed environment, and tree-scoped signalling with
18
+ * SIGTERM→grace→SIGKILL escalation.
19
+ */
20
+ export declare class LocalSubprocessService extends SubprocessService {
21
+ /** Live handles retained only so disposal can terminate and join them. */
22
+ private live;
23
+ /** Live terminal sessions retained through whole-session quiescence. */
24
+ private terminals;
25
+ /** Test hook: spill and platform knobs forwarded to spawnSubprocess. */
26
+ internals: SpawnInternals;
27
+ /** Test hook for platform process inspection; production resolves lazily on terminal spawn. */
28
+ terminalInspector: ProcessInspector | undefined;
29
+ constructor(ctx: Context);
30
+ resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
31
+ private executableCandidates;
32
+ spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
33
+ spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>;
34
+ }
35
+ export default LocalSubprocessService;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-subprocess-local`.
3
+ * @module @deepseek-ai/dsh-subprocess-local/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "subprocess-local-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,64 @@
1
+ /** Platform process-table inspection for terminal readiness, signals, and teardown. */
2
+ import type { SubprocessTerminalSignal } from '@deepseek-ai/dsh-subprocess';
3
+ /** PID plus start identity, preventing teardown escalation after PID reuse. */
4
+ export interface ProcessIdentity {
5
+ pid: number;
6
+ started: string;
7
+ }
8
+ /** Injectable OS process operations used by one local PTY session. */
9
+ export interface ProcessInspector {
10
+ foregroundPgid(shellPid: number): number | undefined;
11
+ isStdinWaiting(pgid: number): boolean;
12
+ /** Return the root and its current transitive descendants, children first. */
13
+ processTree(rootPid: number): ProcessIdentity[];
14
+ /** Return current members of one POSIX process session when the platform exposes them. */
15
+ processSession(sessionId: number): ProcessIdentity[];
16
+ /** Return whether the exact identity remains a non-quiescent process. */
17
+ isAlive(identity: ProcessIdentity): boolean;
18
+ signalGroup(pgid: number, signal: SubprocessTerminalSignal): void;
19
+ signalProcess(identity: ProcessIdentity, signal: 'SIGTERM' | 'SIGKILL'): void;
20
+ }
21
+ /** Testable boundary around filesystem, process-table, and signal syscalls. */
22
+ export interface ProcessInspectorInternals {
23
+ readFile(path: string): string;
24
+ readDir(path: string): string[];
25
+ open(path: string): number;
26
+ read(fd: number, buffer: Buffer, length: number, position: number): number;
27
+ close(fd: number): void;
28
+ exec(file: string, args: string[]): string;
29
+ kill(pid: number, signal: NodeJS.Signals): void;
30
+ }
31
+ interface ProcStat {
32
+ pid: number;
33
+ parentPid: number;
34
+ pgrp: number;
35
+ session: number;
36
+ state: string;
37
+ tpgid: number;
38
+ started: string;
39
+ }
40
+ /**
41
+ * Parse fields used from Linux `/proc/<pid>/stat`, including parenthesized comm text.
42
+ * @param text - complete stat line.
43
+ * @returns Parsed identity/group fields, or undefined for malformed input.
44
+ */
45
+ export declare function parseProcStat(text: string): ProcStat | undefined;
46
+ /**
47
+ * Report whether a Linux process group has an executing member. `false`
48
+ * means the group contains only zombie/dead entries; `undefined` means the
49
+ * process table could not prove either outcome.
50
+ * @param processGroupId - POSIX process-group id to inspect.
51
+ * @param internals - injectable process-table operations.
52
+ * @returns Live-member presence, or `undefined` when unavailable/absent.
53
+ */
54
+ export declare function linuxProcessGroupHasLiveMembers(processGroupId: number, internals?: ProcessInspectorInternals): boolean | undefined;
55
+ /**
56
+ * Create the supported platform inspector or fail at plugin load.
57
+ * @param platform - target Node platform.
58
+ * @param arch - target CPU architecture for Linux syscall numbers.
59
+ * @param internals - filesystem/process boundary, injectable for deterministic tests.
60
+ * @returns Platform process inspector.
61
+ */
62
+ export declare function createProcessInspector(platform?: NodeJS.Platform, arch?: NodeJS.Architecture, internals?: ProcessInspectorInternals): ProcessInspector;
63
+ export {};
64
+ //# sourceMappingURL=process-inspector.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Process plumbing for the local subprocess service: detached process-tree
3
+ * spawn with per-stream stdio dispositions, tail-keep collection with spill
4
+ * files, tree-scoped signalling (POSIX groups; Windows taskkill), and the
5
+ * SIGTERM→SIGKILL escalation. This layer reacts to an abort signal; callers
6
+ * own deadlines, teardown ladders, and cause classification.
7
+ * @module dsh-subprocess-local/spawn
8
+ */
9
+ import type { CollectedOutput, SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
10
+ /**
11
+ * Build a child environment: explicit caller entries override the scrubbed
12
+ * parent base using the target platform's environment-key semantics. A string
13
+ * deliberately restores or overrides an entry; an explicit `undefined`
14
+ * tombstone removes an ordinary ambient entry.
15
+ * @param extra - explicit caller entries and tombstones, merged after the scrub.
16
+ * @returns the environment to hand to `spawn` for the child process.
17
+ */
18
+ export declare function childEnv(extra?: Readonly<NodeJS.ProcessEnv>): NodeJS.ProcessEnv;
19
+ /** Injectable knobs so tests can exercise spill and platform behavior deterministically. */
20
+ export interface SpawnInternals {
21
+ /** Directory for spill files (defaults to the OS temp dir). */
22
+ spillDir?: string;
23
+ /** Windows tree-termination runner (defaults to `taskkill /PID <pid> /T /F`). */
24
+ taskkill?: (pid: number) => void;
25
+ /** Host platform override for signalling decisions. */
26
+ platform?: NodeJS.Platform;
27
+ /** Linux process-group member probe (defaults to `/proc` inspection). */
28
+ linuxProcessGroupHasLiveMembers?: (processGroupId: number) => boolean | undefined;
29
+ }
30
+ /**
31
+ * Collects one stream with a bounded in-memory tail. With a spill cap, on
32
+ * first overflow a spill file is created and every chunk (including those
33
+ * already collected) is appended there while the full stream remains within
34
+ * the cap; without one, only the in-memory tail is ever retained (the
35
+ * diagnostic-tail shape — a language server's stderr).
36
+ *
37
+ * Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
38
+ * end of command output; the spill file covers the head.
39
+ */
40
+ export declare class OutputCollector {
41
+ private readonly maxBytes;
42
+ private readonly maxSpillBytes;
43
+ private readonly label;
44
+ private readonly spillDir;
45
+ private chunks;
46
+ private bytes;
47
+ private dropped;
48
+ private spillFd;
49
+ private spillFile;
50
+ private spillDisabled;
51
+ /** Total bytes ever pushed (not just retained). */
52
+ private total;
53
+ constructor(maxBytes: number, maxSpillBytes: number | undefined, label: string, spillDir: string);
54
+ /**
55
+ * Ingest one stream chunk, counting it toward the whole-stream total. On
56
+ * first overflow of the in-memory cap a spill file is opened (when spilling
57
+ * is enabled) and every chunk (already-collected ones included) is appended
58
+ * there from then on; the in-memory tail then drops whole chunks from its
59
+ * head (or the head of a single over-cap chunk) until it fits the cap again.
60
+ * @param chunk - the raw bytes from one stream 'data' event.
61
+ */
62
+ push(chunk: Buffer): void;
63
+ /** Open the spill file lazily and append `chunk` (and any prior chunks once). */
64
+ private spillAll;
65
+ /** Stop spilling and remove the file once it can no longer hold the complete stream. */
66
+ private discardSpill;
67
+ /**
68
+ * Incremental read in whole-stream byte coordinates: returns everything
69
+ * pushed since `fromByte`. When `fromByte` has already slid out of the
70
+ * in-memory tail window, the read is `lossy` — it returns the whole
71
+ * retained tail and the gap is only recoverable from the spill file.
72
+ * @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
73
+ * @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
74
+ */
75
+ readFrom(fromByte: number): {
76
+ text: string;
77
+ nextOffset: number;
78
+ lossy: boolean;
79
+ spillPath?: string;
80
+ };
81
+ /**
82
+ * Close the spill file once the stream has ended. A failed close (delayed
83
+ * writeback fault) stops advertising the spill path — the file may be
84
+ * missing its tail — while every in-memory read keeps working. Idempotent;
85
+ * the spawn path seals both collectors at settlement so reads after exit
86
+ * never point at a still-open file.
87
+ */
88
+ seal(): void;
89
+ /**
90
+ * Seal the spill file and return the final output.
91
+ * @returns the final collected output: tail text, truncation flag, and the spill path when intact.
92
+ */
93
+ finalize(): CollectedOutput;
94
+ }
95
+ /**
96
+ * Send `sig` to a detached POSIX process group. Never throws: delivery races
97
+ * process exit and may run in a timer callback, so failures are contained and
98
+ * a non-positive pid is a no-op.
99
+ * @param pid - the group leader's pid; non-positive means the spawn failed and the call is a no-op.
100
+ * @param sig - the signal to deliver to the whole group.
101
+ */
102
+ export declare function killGroup(pid: number, sig: NodeJS.Signals): void;
103
+ /**
104
+ * Terminate one Windows process tree with `taskkill /T /F`. Contained like
105
+ * POSIX group signalling — delivery races tree exit, so an absent tree, a
106
+ * nonzero status, or a missing taskkill binary must not break idempotent
107
+ * teardown.
108
+ * @param pid - root process id; non-positive is a no-op.
109
+ */
110
+ export declare function taskkillProcessTree(pid: number): void;
111
+ /**
112
+ * Spawn one isolated detached process tree with the spec's per-stream stdio
113
+ * dispositions. Runtime exits resolve `done` as {@link SubprocessOutcome};
114
+ * only spawn failures reject.
115
+ * @param spec - fully resolved argv, cwd, stdio, grace, cancellation, environment.
116
+ * @param internals - test-only spill-directory, platform, and taskkill overrides.
117
+ * @returns live subprocess handle.
118
+ * @throws when `graceMs` cannot be represented by one Node timer.
119
+ */
120
+ export declare function spawnSubprocess(spec: SubprocessSpawnSpec, internals?: SpawnInternals): SubprocessHandle;
121
+ //# sourceMappingURL=spawn.d.ts.map
@@ -0,0 +1,48 @@
1
+ /** Local node-pty terminal-process implementation for the subprocess seam. */
2
+ import { PassThrough } from 'node:stream';
3
+ import type { IPty } from 'node-pty';
4
+ import type { SubprocessOutcome, SubprocessTerminalForeground, SubprocessTerminalHandle, SubprocessTerminalSignal } from '@deepseek-ai/dsh-subprocess';
5
+ import type { ProcessInspector } from './process-inspector.ts';
6
+ /**
7
+ * A local terminal whose process-session ownership stays below the PTY backend.
8
+ * The seam's terminate() promise — no write, inspection, or signal in flight
9
+ * after settlement — holds here without operation tracking only because every
10
+ * handle call completes synchronously under the hood (node-pty write, ps-based
11
+ * inspection). A first genuinely asynchronous step in any handle call must add
12
+ * the tracking a remote provider needs.
13
+ */
14
+ export declare class LocalTerminalHandle implements SubprocessTerminalHandle {
15
+ private readonly terminal;
16
+ private readonly inspector;
17
+ private readonly graceMs;
18
+ readonly pid: number;
19
+ readonly output: PassThrough;
20
+ readonly done: Promise<SubprocessOutcome>;
21
+ private readonly outcome;
22
+ private readonly dataDisposable;
23
+ private readonly exitDisposable;
24
+ private cleanup;
25
+ private exited;
26
+ private trackedDescendants;
27
+ /** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
28
+ private readonly rootIdentity;
29
+ /**
30
+ * @param terminal - allocated node-pty process.
31
+ * @param inspector - platform process/session operations.
32
+ * @param graceMs - TERM-to-KILL and exit-wait grace.
33
+ */
34
+ constructor(terminal: IPty, inspector: ProcessInspector, graceMs: number);
35
+ write(data: string): Promise<void>;
36
+ inspectForeground(): Promise<SubprocessTerminalForeground | undefined>;
37
+ signalForeground(signal: SubprocessTerminalSignal): Promise<number>;
38
+ terminate(): Promise<void>;
39
+ private survivors;
40
+ private descendants;
41
+ private waitForMembers;
42
+ private signalMembers;
43
+ private unionMembers;
44
+ private stopDescendants;
45
+ private stopShell;
46
+ private closeOnce;
47
+ }
48
+ //# sourceMappingURL=terminal.d.ts.map
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-subprocess-local",
3
+ "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/subprocess/subprocess-local"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "scripts/ensure-spawn-helper.mjs",
32
+ "lib/types/**/*.d.ts"
33
+ ],
34
+ "license": "BSD-3-Clause",
35
+ "peerDependencies": {
36
+ "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
38
+ "@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
39
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
40
+ },
41
+ "dependencies": {
42
+ "node-pty": "^1.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
46
+ "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
47
+ "@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
48
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
49
+ },
50
+ "scripts": {
51
+ "postinstall": "node scripts/ensure-spawn-helper.mjs"
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ /** Restore the executable bit stripped from node-pty's prebuilt helper. */
2
+
3
+ import { chmodSync, existsSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const entry = fileURLToPath(import.meta.resolve('node-pty'))
8
+ const packageRoot = dirname(dirname(entry))
9
+ const candidates = [
10
+ join(packageRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
11
+ join(packageRoot, 'build', 'Release', 'spawn-helper'),
12
+ ]
13
+
14
+ for (const helper of candidates) {
15
+ if (existsSync(helper)) chmodSync(helper, 0o755)
16
+ }