toolcraft 0.0.11 → 0.0.13

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.
Files changed (56) hide show
  1. package/dist/cli.js +274 -160
  2. package/dist/renderer.d.ts +8 -2
  3. package/dist/renderer.js +71 -12
  4. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +4 -0
  5. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +132 -0
  6. package/node_modules/@poe-code/design-system/dist/components/help-formatter.d.ts +13 -0
  7. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +116 -7
  8. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +2 -2
  9. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  10. package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
  11. package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
  12. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -2
  13. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  14. package/node_modules/@poe-code/process-runner/README.md +41 -0
  15. package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +2 -0
  16. package/node_modules/@poe-code/process-runner/dist/docker/args.js +40 -0
  17. package/node_modules/@poe-code/process-runner/dist/docker/context.d.ts +3 -0
  18. package/node_modules/@poe-code/process-runner/dist/docker/context.js +30 -0
  19. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.d.ts +28 -0
  20. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +428 -0
  21. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.d.ts +2 -0
  22. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +131 -0
  23. package/node_modules/@poe-code/process-runner/dist/docker/engine.d.ts +3 -0
  24. package/node_modules/@poe-code/process-runner/dist/docker/engine.js +24 -0
  25. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.d.ts +2 -0
  26. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +48 -0
  27. package/node_modules/@poe-code/process-runner/dist/host/host-runner.d.ts +3 -0
  28. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +74 -0
  29. package/node_modules/@poe-code/process-runner/dist/index.d.ts +8 -0
  30. package/node_modules/@poe-code/process-runner/dist/index.js +7 -0
  31. package/node_modules/@poe-code/process-runner/dist/testing/index.d.ts +2 -0
  32. package/node_modules/@poe-code/process-runner/dist/testing/index.js +1 -0
  33. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.d.ts +3 -0
  34. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +115 -0
  35. package/node_modules/@poe-code/process-runner/dist/testing/verify.d.ts +1 -0
  36. package/node_modules/@poe-code/process-runner/dist/testing/verify.js +359 -0
  37. package/node_modules/@poe-code/process-runner/dist/types.d.ts +180 -0
  38. package/node_modules/@poe-code/process-runner/dist/types.js +1 -0
  39. package/node_modules/@poe-code/process-runner/package.json +27 -0
  40. package/node_modules/@poe-code/task-list/README.md +49 -5
  41. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.d.ts +19 -0
  42. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +62 -0
  43. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +13 -0
  44. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +627 -0
  45. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +253 -41
  46. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +7 -1
  47. package/node_modules/@poe-code/task-list/dist/backends/utils.js +21 -0
  48. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +171 -16
  49. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  50. package/node_modules/@poe-code/task-list/dist/index.js +1 -1
  51. package/node_modules/@poe-code/task-list/dist/open.d.ts +4 -2
  52. package/node_modules/@poe-code/task-list/dist/open.js +27 -3
  53. package/node_modules/@poe-code/task-list/dist/types.d.ts +51 -3
  54. package/node_modules/@poe-code/task-list/dist/types.js +25 -0
  55. package/node_modules/@poe-code/task-list/package.json +1 -0
  56. package/package.json +11 -4
@@ -0,0 +1,74 @@
1
+ import { spawn as spawnChildProcess } from "node:child_process";
2
+ export function createHostRunner(options = {}) {
3
+ const detached = options.detached === true;
4
+ return {
5
+ name: "host",
6
+ exec(spec) {
7
+ const stdinMode = spec.stdin ?? "ignore";
8
+ const stdoutMode = spec.stdout ?? "pipe";
9
+ const stderrMode = spec.stderr ?? "pipe";
10
+ const stdio = stdinMode === "inherit" && stdoutMode === "inherit" && stderrMode === "inherit"
11
+ ? "inherit"
12
+ : [stdinMode, stdoutMode, stderrMode];
13
+ const child = spawnChildProcess(spec.command, spec.args ?? [], {
14
+ cwd: spec.cwd,
15
+ env: spec.env,
16
+ stdio,
17
+ ...(detached ? { detached: true } : {})
18
+ });
19
+ if (detached) {
20
+ child.unref();
21
+ }
22
+ const kill = (signal) => {
23
+ if (detached && process.platform !== "win32" && child.pid !== undefined) {
24
+ process.kill(-child.pid, signal);
25
+ return;
26
+ }
27
+ child.kill(signal);
28
+ };
29
+ let settled = false;
30
+ let resolveResult = null;
31
+ const result = new Promise((resolve) => {
32
+ resolveResult = resolve;
33
+ });
34
+ const cleanupAbort = bindAbortSignal(spec.signal, () => {
35
+ kill("SIGTERM");
36
+ });
37
+ child.once("close", (code) => {
38
+ if (settled)
39
+ return;
40
+ settled = true;
41
+ cleanupAbort();
42
+ resolveResult?.({ exitCode: code ?? 1 });
43
+ });
44
+ child.once("error", () => {
45
+ if (settled)
46
+ return;
47
+ settled = true;
48
+ cleanupAbort();
49
+ resolveResult?.({ exitCode: 1 });
50
+ });
51
+ return {
52
+ pid: child.pid ?? null,
53
+ stdin: child.stdin,
54
+ stdout: child.stdout,
55
+ stderr: child.stderr,
56
+ result,
57
+ kill
58
+ };
59
+ }
60
+ };
61
+ }
62
+ function bindAbortSignal(signal, onAbort) {
63
+ if (signal === undefined) {
64
+ return () => { };
65
+ }
66
+ if (signal.aborted) {
67
+ onAbort();
68
+ return () => { };
69
+ }
70
+ signal.addEventListener("abort", onAbort, { once: true });
71
+ return () => {
72
+ signal.removeEventListener("abort", onAbort);
73
+ };
74
+ }
@@ -0,0 +1,8 @@
1
+ export { buildContextArgs, detectContext } from "./docker/context.js";
2
+ export { detectEngine, isEngineAvailable } from "./docker/engine.js";
3
+ export { createDockerRunner } from "./docker/docker-runner.js";
4
+ export { buildDockerRuntimeTemplate, dockerExecutionEnvFactory } from "./docker/docker-execution-env.js";
5
+ export { hostExecutionEnvFactory } from "./host/host-execution-env.js";
6
+ export { createHostRunner } from "./host/host-runner.js";
7
+ export { createMockRunner, createMockRunnerByCommand } from "./testing/index.js";
8
+ export type { DownloadResult, DockerMount, DockerPortMapping, DockerRunArgs, DockerRunnerOptions, Engine, ExecutionState, ExecutionEnvFactory, ExecutionEnvType, HostRunnerOptions, JobHandle, JobStatus, LogChunk, MockRunBehavior, OpenedEnv, OpenSpec, RunHandle, RunResult, Runner, RunSpec, TemplateEntry, UploadResult } from "./types.js";
@@ -0,0 +1,7 @@
1
+ export { buildContextArgs, detectContext } from "./docker/context.js";
2
+ export { detectEngine, isEngineAvailable } from "./docker/engine.js";
3
+ export { createDockerRunner } from "./docker/docker-runner.js";
4
+ export { buildDockerRuntimeTemplate, dockerExecutionEnvFactory } from "./docker/docker-execution-env.js";
5
+ export { hostExecutionEnvFactory } from "./host/host-execution-env.js";
6
+ export { createHostRunner } from "./host/host-runner.js";
7
+ export { createMockRunner, createMockRunnerByCommand } from "./testing/index.js";
@@ -0,0 +1,2 @@
1
+ export { createMockRunner, createMockRunnerByCommand } from "./mock-runner.js";
2
+ export type { MockRunBehavior } from "../types.js";
@@ -0,0 +1 @@
1
+ export { createMockRunner, createMockRunnerByCommand } from "./mock-runner.js";
@@ -0,0 +1,3 @@
1
+ import type { MockRunBehavior, Runner } from "../types.js";
2
+ export declare function createMockRunner(behaviors: MockRunBehavior[]): Runner;
3
+ export declare function createMockRunnerByCommand(behaviorsByCommand: Record<string, MockRunBehavior>): Runner;
@@ -0,0 +1,115 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ export function createMockRunner(behaviors) {
3
+ const remaining = [...behaviors];
4
+ return {
5
+ name: "mock",
6
+ exec(spec) {
7
+ const behavior = remaining.shift();
8
+ if (behavior === undefined) {
9
+ throw new Error("No mock run behaviors left");
10
+ }
11
+ return createRunHandle(spec, behavior);
12
+ }
13
+ };
14
+ }
15
+ export function createMockRunnerByCommand(behaviorsByCommand) {
16
+ return {
17
+ name: "mock",
18
+ exec(spec) {
19
+ const behavior = behaviorsByCommand[spec.command];
20
+ if (behavior === undefined) {
21
+ throw new Error(`No mock run behavior found for command "${spec.command}"`);
22
+ }
23
+ return createRunHandle(spec, behavior);
24
+ }
25
+ };
26
+ }
27
+ function createRunHandle(spec, behavior) {
28
+ const stdoutMode = spec.stdout ?? "pipe";
29
+ const stderrMode = spec.stderr ?? "pipe";
30
+ const stdinMode = spec.stdin ?? "ignore";
31
+ const interval = behavior.stdoutInterval ?? 10;
32
+ const stdoutController = stdoutMode === "pipe" && behavior.stdout !== undefined
33
+ ? createReadableStream(behavior.stdout, interval)
34
+ : null;
35
+ const stderrController = stderrMode === "pipe" && behavior.stderr !== undefined
36
+ ? createReadableStream(behavior.stderr, interval)
37
+ : null;
38
+ let resolveResult = null;
39
+ const result = new Promise((resolve) => {
40
+ resolveResult = resolve;
41
+ });
42
+ let finished = false;
43
+ const complete = () => {
44
+ if (finished || resolveResult === null) {
45
+ return;
46
+ }
47
+ finished = true;
48
+ resolveResult({ exitCode: behavior.exitCode });
49
+ };
50
+ const stopStreams = () => {
51
+ stdoutController?.stop();
52
+ stderrController?.stop();
53
+ };
54
+ const exitAfterMs = behavior.exitAfterMs ?? 0;
55
+ const exitTimer = exitAfterMs > 0
56
+ ? setTimeout(complete, exitAfterMs)
57
+ : queueMicrotask(complete);
58
+ return {
59
+ pid: behavior.pid ?? null,
60
+ stdout: stdoutController?.stream ?? null,
61
+ stderr: stderrController?.stream ?? null,
62
+ stdin: stdinMode === "pipe" ? createWritableStream() : null,
63
+ result,
64
+ kill() {
65
+ if (typeof exitTimer === "object" && exitTimer !== null && "hasRef" in exitTimer) {
66
+ clearTimeout(exitTimer);
67
+ }
68
+ stopStreams();
69
+ complete();
70
+ }
71
+ };
72
+ }
73
+ function createReadableStream(lines, interval) {
74
+ const stream = new Readable({
75
+ read() { }
76
+ });
77
+ const timers = new Set();
78
+ let stopped = false;
79
+ const stop = () => {
80
+ if (stopped) {
81
+ return;
82
+ }
83
+ stopped = true;
84
+ for (const timer of timers) {
85
+ clearTimeout(timer);
86
+ }
87
+ timers.clear();
88
+ stream.push(null);
89
+ };
90
+ if (lines.length === 0) {
91
+ queueMicrotask(stop);
92
+ return { stream, stop };
93
+ }
94
+ for (const [index, line] of lines.entries()) {
95
+ const timer = setTimeout(() => {
96
+ timers.delete(timer);
97
+ if (stopped) {
98
+ return;
99
+ }
100
+ stream.push(line);
101
+ if (index === lines.length - 1) {
102
+ stop();
103
+ }
104
+ }, interval * (index + 1));
105
+ timers.add(timer);
106
+ }
107
+ return { stream, stop };
108
+ }
109
+ function createWritableStream() {
110
+ return new Writable({
111
+ write(_chunk, _encoding, callback) {
112
+ callback();
113
+ }
114
+ });
115
+ }
@@ -0,0 +1,359 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createDockerRunner, createHostRunner } from "../index.js";
7
+ const dockerPortHost = 18923;
8
+ async function verifyHostPiped() {
9
+ const runner = createHostRunner();
10
+ const handle = runner.exec({
11
+ command: "echo",
12
+ args: ["hello"],
13
+ stdout: "pipe",
14
+ stderr: "pipe"
15
+ });
16
+ const stdoutPromise = readStream(handle.stdout, "host piped stdout");
17
+ const { exitCode } = await handle.result;
18
+ const stdout = await stdoutPromise;
19
+ assert.equal(exitCode, 0, "host piped: exit code 0");
20
+ assert.equal(stdout.trim(), "hello", "host piped: stdout captured");
21
+ console.log("✓ host runner — piped mode");
22
+ }
23
+ async function verifyHostStdin() {
24
+ const runner = createHostRunner();
25
+ const handle = runner.exec({
26
+ command: "cat",
27
+ stdin: "pipe",
28
+ stdout: "pipe"
29
+ });
30
+ const stdoutPromise = readStream(handle.stdout, "host stdin stdout");
31
+ const stdin = assertWritable(handle.stdin, "host stdin: stdin available");
32
+ stdin.write("ping");
33
+ stdin.end();
34
+ const { exitCode } = await handle.result;
35
+ const stdout = await stdoutPromise;
36
+ assert.equal(exitCode, 0, "host stdin: exit code 0");
37
+ assert.equal(stdout, "ping", "host stdin: stdin echoed to stdout");
38
+ console.log("✓ host runner — stdin pipe");
39
+ }
40
+ async function verifyHostKill() {
41
+ const runner = createHostRunner();
42
+ const handle = runner.exec({
43
+ command: "sleep",
44
+ args: ["60"],
45
+ stdout: "pipe",
46
+ stderr: "pipe"
47
+ });
48
+ await delay(100);
49
+ handle.kill("SIGTERM");
50
+ const { exitCode } = await handle.result;
51
+ assert.notEqual(exitCode, 0, "host kill: non-zero exit after SIGTERM");
52
+ console.log("✓ host runner — kill");
53
+ }
54
+ async function verifyHostAbort() {
55
+ const runner = createHostRunner();
56
+ const controller = new AbortController();
57
+ const handle = runner.exec({
58
+ command: "sleep",
59
+ args: ["60"],
60
+ stdout: "pipe",
61
+ stderr: "pipe",
62
+ signal: controller.signal
63
+ });
64
+ await delay(100);
65
+ controller.abort();
66
+ const { exitCode } = await handle.result;
67
+ assert.notEqual(exitCode, 0, "host abort: non-zero exit after abort");
68
+ console.log("✓ host runner — abort signal");
69
+ }
70
+ async function verifyHostInherit() {
71
+ const runner = createHostRunner();
72
+ const handle = runner.exec({
73
+ command: "echo",
74
+ args: ["inherit-mode-output"],
75
+ stdin: "inherit",
76
+ stdout: "inherit",
77
+ stderr: "inherit"
78
+ });
79
+ assert.equal(handle.stdout, null, "host inherit: stdout is null");
80
+ assert.equal(handle.stderr, null, "host inherit: stderr is null");
81
+ assert.equal(handle.stdin, null, "host inherit: stdin is null");
82
+ const { exitCode } = await handle.result;
83
+ assert.equal(exitCode, 0, "host inherit: exit code 0");
84
+ console.log("✓ host runner — inherit mode");
85
+ }
86
+ async function verifyHostExitCode() {
87
+ const runner = createHostRunner();
88
+ const handle = runner.exec({
89
+ command: "sh",
90
+ args: ["-c", "exit 42"],
91
+ stdout: "pipe",
92
+ stderr: "pipe"
93
+ });
94
+ const { exitCode } = await handle.result;
95
+ assert.equal(exitCode, 42, "host exit code: 42");
96
+ console.log("✓ host runner — non-zero exit code");
97
+ }
98
+ async function verifyHostEnv() {
99
+ const runner = createHostRunner();
100
+ const handle = runner.exec({
101
+ command: "sh",
102
+ args: ["-c", "printf %s \"$MY_TEST_VAR\""],
103
+ stdout: "pipe",
104
+ env: { ...process.env, MY_TEST_VAR: "runner-works" }
105
+ });
106
+ const stdoutPromise = readStream(handle.stdout, "host env stdout");
107
+ const { exitCode } = await handle.result;
108
+ const stdout = await stdoutPromise;
109
+ assert.equal(exitCode, 0, "host env: exit code 0");
110
+ assert.equal(stdout, "runner-works", "host env: env var passed");
111
+ console.log("✓ host runner — env vars");
112
+ }
113
+ async function verifyDockerPiped(engine) {
114
+ const runner = createDockerRunner({ image: "alpine:latest", engine, context: "" });
115
+ const handle = runner.exec({
116
+ command: "echo",
117
+ args: ["hello from docker"],
118
+ stdout: "pipe",
119
+ stderr: "pipe"
120
+ });
121
+ const stdoutPromise = readStream(handle.stdout, "docker piped stdout");
122
+ const { exitCode } = await handle.result;
123
+ const stdout = await stdoutPromise;
124
+ assert.equal(exitCode, 0, "docker piped: exit code 0");
125
+ assert.equal(stdout.trim(), "hello from docker", "docker piped: stdout captured");
126
+ console.log("✓ docker runner — piped mode");
127
+ }
128
+ async function verifyDockerStdin(engine) {
129
+ const runner = createDockerRunner({ image: "alpine:latest", engine, context: "" });
130
+ const handle = runner.exec({
131
+ command: "cat",
132
+ stdin: "pipe",
133
+ stdout: "pipe"
134
+ });
135
+ const stdoutPromise = readStream(handle.stdout, "docker stdin stdout");
136
+ const stdin = assertWritable(handle.stdin, "docker stdin: stdin available");
137
+ stdin.write("docker-ping");
138
+ stdin.end();
139
+ const { exitCode } = await handle.result;
140
+ const stdout = await stdoutPromise;
141
+ assert.equal(exitCode, 0, "docker stdin: exit code 0");
142
+ assert.equal(stdout, "docker-ping", "docker stdin: stdin echoed to stdout");
143
+ console.log("✓ docker runner — stdin pipe");
144
+ }
145
+ async function verifyDockerKill(engine) {
146
+ const runner = createDockerRunner({ image: "alpine:latest", engine, context: "" });
147
+ const handle = runner.exec({
148
+ command: "sleep",
149
+ args: ["60"],
150
+ stdout: "pipe",
151
+ stderr: "pipe"
152
+ });
153
+ await delay(1000);
154
+ handle.kill("SIGTERM");
155
+ const { exitCode } = await handle.result;
156
+ assert.notEqual(exitCode, 0, "docker kill: non-zero exit after SIGTERM");
157
+ console.log("✓ docker runner — kill");
158
+ }
159
+ async function verifyDockerExitCode(engine) {
160
+ const runner = createDockerRunner({ image: "alpine:latest", engine, context: "" });
161
+ const handle = runner.exec({
162
+ command: "sh",
163
+ args: ["-c", "exit 42"],
164
+ stdout: "pipe",
165
+ stderr: "pipe"
166
+ });
167
+ const { exitCode } = await handle.result;
168
+ assert.equal(exitCode, 42, "docker exit code: 42");
169
+ console.log("✓ docker runner — non-zero exit code");
170
+ }
171
+ async function verifyDockerEnv(engine) {
172
+ const runner = createDockerRunner({ image: "alpine:latest", engine, context: "" });
173
+ const handle = runner.exec({
174
+ command: "sh",
175
+ args: ["-c", "printf %s \"$MY_TEST_VAR\""],
176
+ stdout: "pipe",
177
+ env: { MY_TEST_VAR: "docker-runner-works" }
178
+ });
179
+ const stdoutPromise = readStream(handle.stdout, "docker env stdout");
180
+ const { exitCode } = await handle.result;
181
+ const stdout = await stdoutPromise;
182
+ assert.equal(exitCode, 0, "docker env: exit code 0");
183
+ assert.equal(stdout, "docker-runner-works", "docker env: env var passed");
184
+ console.log("✓ docker runner — env vars");
185
+ }
186
+ async function verifyDockerMount(engine) {
187
+ const mountRoot = mkdtempSync(path.join(tmpdir(), "process-runner-verify-"));
188
+ const mountFile = path.join(mountRoot, "mounted.txt");
189
+ writeFileSync(mountFile, "mounted-ok\n", "utf8");
190
+ try {
191
+ const runner = createDockerRunner({
192
+ image: "alpine:latest",
193
+ engine,
194
+ context: "",
195
+ mounts: [{ source: mountRoot, target: "/host-data", readonly: true }]
196
+ });
197
+ const handle = runner.exec({
198
+ command: "cat",
199
+ args: ["/host-data/mounted.txt"],
200
+ stdout: "pipe",
201
+ stderr: "pipe"
202
+ });
203
+ const stdoutPromise = readStream(handle.stdout, "docker mount stdout");
204
+ const { exitCode } = await handle.result;
205
+ const stdout = await stdoutPromise;
206
+ assert.equal(exitCode, 0, "docker mount: exit code 0");
207
+ assert.equal(stdout.trim(), "mounted-ok", "docker mount: mounted file available");
208
+ console.log("✓ docker runner — bind mount");
209
+ }
210
+ finally {
211
+ rmSync(mountRoot, { recursive: true, force: true });
212
+ }
213
+ }
214
+ async function verifyDockerPort(engine) {
215
+ const runner = createDockerRunner({
216
+ image: "alpine:latest",
217
+ engine,
218
+ context: "",
219
+ ports: [{ host: dockerPortHost, container: 8080 }]
220
+ });
221
+ const handle = runner.exec({
222
+ command: "sh",
223
+ args: [
224
+ "-c",
225
+ "mkdir -p /srv/http && printf ok > /srv/http/index.html && exec busybox httpd -f -p 8080 -h /srv/http"
226
+ ],
227
+ stdout: "pipe",
228
+ stderr: "pipe"
229
+ });
230
+ try {
231
+ await waitForHttpOk(`http://127.0.0.1:${dockerPortHost}`);
232
+ console.log("✓ docker runner — port mapping");
233
+ }
234
+ finally {
235
+ handle.kill("SIGTERM");
236
+ await handle.result;
237
+ }
238
+ }
239
+ async function verifyDockerInteractiveContract(engine) {
240
+ const runner = createDockerRunner({ image: "alpine:latest", engine, context: "" });
241
+ const handle = runner.exec({
242
+ command: "sh",
243
+ args: [
244
+ "-c",
245
+ "if [ -t 0 ]; then printf stdin-is-tty; else printf stdin-not-tty; fi; printf '\\n'; if [ -t 1 ]; then printf stdout-is-tty; else printf stdout-not-tty; fi"
246
+ ],
247
+ stdin: "pipe",
248
+ stdout: "pipe",
249
+ tty: true
250
+ });
251
+ const stdoutPromise = readStream(handle.stdout, "docker interactive stdout");
252
+ const stdin = assertWritable(handle.stdin, "docker interactive: stdin available");
253
+ stdin.end();
254
+ const { exitCode } = await handle.result;
255
+ const stdout = await stdoutPromise;
256
+ assert.equal(exitCode, 0, "docker interactive: exit code 0");
257
+ assert.ok(stdout.includes("stdin-is-tty"), "docker interactive: stdin is tty");
258
+ assert.ok(stdout.includes("stdout-is-tty"), "docker interactive: stdout is tty");
259
+ console.log("✓ docker runner — tty contract");
260
+ }
261
+ async function main() {
262
+ console.log("\n=== Host Runner ===\n");
263
+ await verifyHostPiped();
264
+ await verifyHostStdin();
265
+ await verifyHostKill();
266
+ await verifyHostAbort();
267
+ await verifyHostInherit();
268
+ await verifyHostExitCode();
269
+ await verifyHostEnv();
270
+ console.log("\n=== Docker Runner ===\n");
271
+ const engine = resolveAvailableEngine();
272
+ if (engine !== null) {
273
+ await verifyDockerPiped(engine);
274
+ await verifyDockerStdin(engine);
275
+ await verifyDockerKill(engine);
276
+ await verifyDockerExitCode(engine);
277
+ await verifyDockerEnv(engine);
278
+ await verifyDockerMount(engine);
279
+ await verifyDockerPort(engine);
280
+ await verifyDockerInteractiveContract(engine);
281
+ }
282
+ else {
283
+ console.log("⏭ Docker not available — skipping docker runner tests");
284
+ }
285
+ console.log("\n=== All verifications passed ===\n");
286
+ }
287
+ async function readStream(stream, label) {
288
+ const readable = assertReadable(stream, `${label}: stream is available`);
289
+ return await new Promise((resolve, reject) => {
290
+ let output = "";
291
+ readable.setEncoding("utf8");
292
+ readable.on("data", (chunk) => {
293
+ output += chunk;
294
+ });
295
+ readable.once("end", () => {
296
+ resolve(output);
297
+ });
298
+ readable.once("error", reject);
299
+ });
300
+ }
301
+ function assertReadable(stream, message) {
302
+ if (stream === null) {
303
+ assert.fail(message);
304
+ }
305
+ return stream;
306
+ }
307
+ function assertWritable(stream, message) {
308
+ if (stream === null) {
309
+ assert.fail(message);
310
+ }
311
+ return stream;
312
+ }
313
+ function resolveAvailableEngine() {
314
+ for (const engine of ["docker", "podman"]) {
315
+ if (!isEngineResponsive(engine, ["--version"])) {
316
+ continue;
317
+ }
318
+ if (isEngineResponsive(engine, ["info"])) {
319
+ return engine;
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+ function isEngineResponsive(engine, args) {
325
+ const result = spawnSync(engine, args, {
326
+ stdio: "ignore",
327
+ timeout: 2000
328
+ });
329
+ if (result.error !== undefined) {
330
+ return false;
331
+ }
332
+ return result.status === 0;
333
+ }
334
+ async function waitForHttpOk(url) {
335
+ const startedAt = Date.now();
336
+ while (Date.now() - startedAt < 15000) {
337
+ try {
338
+ const response = await fetch(url);
339
+ const body = await response.text();
340
+ if (response.ok && body.includes("ok")) {
341
+ return;
342
+ }
343
+ }
344
+ catch (error) {
345
+ void error;
346
+ }
347
+ await delay(250);
348
+ }
349
+ throw new Error(`Timed out waiting for ${url}`);
350
+ }
351
+ function delay(ms) {
352
+ return new Promise((resolve) => {
353
+ setTimeout(resolve, ms);
354
+ });
355
+ }
356
+ main().catch((error) => {
357
+ console.error(error);
358
+ process.exit(1);
359
+ });