toolcraft 0.0.12 → 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.
- package/node_modules/@poe-code/process-runner/README.md +41 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +40 -0
- package/node_modules/@poe-code/process-runner/dist/docker/context.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/docker/context.js +30 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.d.ts +28 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +428 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +131 -0
- package/node_modules/@poe-code/process-runner/dist/docker/engine.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/docker/engine.js +24 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +48 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +74 -0
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +8 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +7 -0
- package/node_modules/@poe-code/process-runner/dist/testing/index.d.ts +2 -0
- package/node_modules/@poe-code/process-runner/dist/testing/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +115 -0
- package/node_modules/@poe-code/process-runner/dist/testing/verify.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/verify.js +359 -0
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +180 -0
- package/node_modules/@poe-code/process-runner/dist/types.js +1 -0
- package/node_modules/@poe-code/process-runner/package.json +27 -0
- package/package.json +10 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { buildDockerRunArgs } from "./args.js";
|
|
4
|
+
import { buildContextArgs, detectContext } from "./context.js";
|
|
5
|
+
import { detectEngine } from "./engine.js";
|
|
6
|
+
export function createDockerRunner(options) {
|
|
7
|
+
const engine = options.engine ?? detectEngine();
|
|
8
|
+
const context = options.context ?? detectContext();
|
|
9
|
+
return {
|
|
10
|
+
name: "docker",
|
|
11
|
+
exec(spec) {
|
|
12
|
+
const stdinMode = spec.stdin ?? "ignore";
|
|
13
|
+
const stdoutMode = spec.stdout ?? "pipe";
|
|
14
|
+
const stderrMode = spec.stderr ?? "pipe";
|
|
15
|
+
const interactiveMode = stdinMode === "inherit" &&
|
|
16
|
+
stdoutMode === "inherit" &&
|
|
17
|
+
stderrMode === "inherit" &&
|
|
18
|
+
spec.tty === true;
|
|
19
|
+
const containerName = buildContainerName(options.containerName ?? spec.command);
|
|
20
|
+
const runArgs = buildDockerRunArgs({
|
|
21
|
+
engine,
|
|
22
|
+
context,
|
|
23
|
+
image: options.image,
|
|
24
|
+
command: spec.command,
|
|
25
|
+
args: spec.args ?? [],
|
|
26
|
+
cwd: spec.cwd,
|
|
27
|
+
env: spec.env,
|
|
28
|
+
mounts: options.mounts ?? [],
|
|
29
|
+
ports: options.ports ?? [],
|
|
30
|
+
network: options.network,
|
|
31
|
+
containerName,
|
|
32
|
+
detached: false,
|
|
33
|
+
interactive: stdinMode === "pipe" || stdinMode === "inherit",
|
|
34
|
+
tty: spec.tty ?? false,
|
|
35
|
+
rm: true,
|
|
36
|
+
extraArgs: options.extraArgs ?? []
|
|
37
|
+
});
|
|
38
|
+
const [command, ...args] = runArgs;
|
|
39
|
+
const child = childProcess.spawn(command, args, {
|
|
40
|
+
stdio: interactiveMode ? "inherit" : [stdinMode, stdoutMode, stderrMode]
|
|
41
|
+
});
|
|
42
|
+
let isResultSettled = false;
|
|
43
|
+
let resolveResult = null;
|
|
44
|
+
const result = new Promise((resolve) => {
|
|
45
|
+
resolveResult = resolve;
|
|
46
|
+
});
|
|
47
|
+
const cleanupAbort = bindAbortSignal(spec.signal, () => {
|
|
48
|
+
spawnControlCommand(engine, context, ["stop", containerName]);
|
|
49
|
+
});
|
|
50
|
+
const settleResult = (exitCode) => {
|
|
51
|
+
if (isResultSettled) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
isResultSettled = true;
|
|
55
|
+
cleanupAbort();
|
|
56
|
+
resolveResult?.({ exitCode });
|
|
57
|
+
};
|
|
58
|
+
child.once("error", () => {
|
|
59
|
+
settleResult(1);
|
|
60
|
+
});
|
|
61
|
+
child.once("close", (code) => {
|
|
62
|
+
settleResult(code ?? 1);
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
pid: null,
|
|
66
|
+
stdin: interactiveMode ? null : child.stdin,
|
|
67
|
+
stdout: interactiveMode ? null : child.stdout,
|
|
68
|
+
stderr: interactiveMode ? null : child.stderr,
|
|
69
|
+
result,
|
|
70
|
+
kill(signal) {
|
|
71
|
+
if (signal === "SIGKILL") {
|
|
72
|
+
spawnControlCommand(engine, context, ["kill", containerName]);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (signal === undefined || signal === "SIGTERM") {
|
|
76
|
+
spawnControlCommand(engine, context, ["stop", containerName]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
spawnControlCommand(engine, context, ["kill", `--signal=${signal}`, containerName]);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function buildContainerName(name) {
|
|
86
|
+
const suffix = randomBytes(3).toString("hex").slice(0, 6);
|
|
87
|
+
const sanitizedName = sanitizeContainerName(name);
|
|
88
|
+
return `poe-run-${sanitizedName}-${suffix}`;
|
|
89
|
+
}
|
|
90
|
+
function sanitizeContainerName(name) {
|
|
91
|
+
let sanitized = "";
|
|
92
|
+
for (const char of name) {
|
|
93
|
+
if (isContainerNameCharacter(char)) {
|
|
94
|
+
sanitized += char;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
sanitized += "-";
|
|
98
|
+
}
|
|
99
|
+
return sanitized.length > 0 ? sanitized : "command";
|
|
100
|
+
}
|
|
101
|
+
function isContainerNameCharacter(char) {
|
|
102
|
+
const code = char.charCodeAt(0);
|
|
103
|
+
if (code >= 48 && code <= 57) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (code >= 65 && code <= 90) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (code >= 97 && code <= 122) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return char === "." || char === "_" || char === "-";
|
|
113
|
+
}
|
|
114
|
+
function spawnControlCommand(engine, context, args) {
|
|
115
|
+
childProcess.spawn(engine, [...buildContextArgs(engine, context), ...args], {
|
|
116
|
+
stdio: "ignore"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function bindAbortSignal(signal, onAbort) {
|
|
120
|
+
if (signal === undefined) {
|
|
121
|
+
return () => { };
|
|
122
|
+
}
|
|
123
|
+
if (signal.aborted) {
|
|
124
|
+
onAbort();
|
|
125
|
+
return () => { };
|
|
126
|
+
}
|
|
127
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
return () => {
|
|
129
|
+
signal.removeEventListener("abort", onAbort);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
export function detectEngine() {
|
|
3
|
+
if (isEngineAvailable("docker")) {
|
|
4
|
+
return "docker";
|
|
5
|
+
}
|
|
6
|
+
if (isEngineAvailable("podman")) {
|
|
7
|
+
return "podman";
|
|
8
|
+
}
|
|
9
|
+
throw new Error("No container engine found. Please install Docker or Podman:\n" +
|
|
10
|
+
" - Docker Desktop: https://www.docker.com/products/docker-desktop\n" +
|
|
11
|
+
" - Colima (macOS): brew install colima && colima start\n" +
|
|
12
|
+
" - Podman: https://podman.io/docs/installation");
|
|
13
|
+
}
|
|
14
|
+
export function isEngineAvailable(engine) {
|
|
15
|
+
try {
|
|
16
|
+
execSync(`${engine} --version`, {
|
|
17
|
+
stdio: "ignore"
|
|
18
|
+
});
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHostRunner } from "./host-runner.js";
|
|
2
|
+
export const hostExecutionEnvFactory = {
|
|
3
|
+
type: "host",
|
|
4
|
+
supportsDetach: false,
|
|
5
|
+
async open(openSpec) {
|
|
6
|
+
return {
|
|
7
|
+
id: "host",
|
|
8
|
+
job: null,
|
|
9
|
+
async uploadWorkspace() {
|
|
10
|
+
return {
|
|
11
|
+
files: 0,
|
|
12
|
+
bytes: 0,
|
|
13
|
+
skipped: []
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
async downloadWorkspace() {
|
|
17
|
+
return {
|
|
18
|
+
files: 0,
|
|
19
|
+
bytes: 0,
|
|
20
|
+
conflicts: []
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
exec(spec) {
|
|
24
|
+
return createHostRunner().exec(spec);
|
|
25
|
+
},
|
|
26
|
+
async detach() {
|
|
27
|
+
throw new Error("host runtime does not support detach because host has no addressable env");
|
|
28
|
+
},
|
|
29
|
+
shell() {
|
|
30
|
+
const shellSpec = openSpec.shellSpec;
|
|
31
|
+
return createHostRunner().exec({
|
|
32
|
+
command: shellSpec?.command ?? openSpec.env.SHELL ?? process.env.SHELL ?? "sh",
|
|
33
|
+
...(shellSpec?.args ? { args: shellSpec.args } : {}),
|
|
34
|
+
cwd: openSpec.cwd,
|
|
35
|
+
env: shellSpec && "env" in shellSpec ? shellSpec.env : openSpec.env,
|
|
36
|
+
stdin: "inherit",
|
|
37
|
+
stdout: "inherit",
|
|
38
|
+
stderr: "inherit",
|
|
39
|
+
tty: true
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
async close() { }
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
async attach() {
|
|
46
|
+
throw new Error("host runtime does not support reattach");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -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 @@
|
|
|
1
|
+
export { createMockRunner, createMockRunnerByCommand } from "./mock-runner.js";
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|