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.
Files changed (27) hide show
  1. package/node_modules/@poe-code/process-runner/README.md +41 -0
  2. package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +2 -0
  3. package/node_modules/@poe-code/process-runner/dist/docker/args.js +40 -0
  4. package/node_modules/@poe-code/process-runner/dist/docker/context.d.ts +3 -0
  5. package/node_modules/@poe-code/process-runner/dist/docker/context.js +30 -0
  6. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.d.ts +28 -0
  7. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +428 -0
  8. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.d.ts +2 -0
  9. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +131 -0
  10. package/node_modules/@poe-code/process-runner/dist/docker/engine.d.ts +3 -0
  11. package/node_modules/@poe-code/process-runner/dist/docker/engine.js +24 -0
  12. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.d.ts +2 -0
  13. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +48 -0
  14. package/node_modules/@poe-code/process-runner/dist/host/host-runner.d.ts +3 -0
  15. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +74 -0
  16. package/node_modules/@poe-code/process-runner/dist/index.d.ts +8 -0
  17. package/node_modules/@poe-code/process-runner/dist/index.js +7 -0
  18. package/node_modules/@poe-code/process-runner/dist/testing/index.d.ts +2 -0
  19. package/node_modules/@poe-code/process-runner/dist/testing/index.js +1 -0
  20. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.d.ts +3 -0
  21. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +115 -0
  22. package/node_modules/@poe-code/process-runner/dist/testing/verify.d.ts +1 -0
  23. package/node_modules/@poe-code/process-runner/dist/testing/verify.js +359 -0
  24. package/node_modules/@poe-code/process-runner/dist/types.d.ts +180 -0
  25. package/node_modules/@poe-code/process-runner/dist/types.js +1 -0
  26. package/node_modules/@poe-code/process-runner/package.json +27 -0
  27. package/package.json +10 -4
@@ -0,0 +1,41 @@
1
+ # @poe-code/process-runner
2
+
3
+ Low-level process execution abstraction. Single interface for launching processes on the host or inside Docker containers.
4
+
5
+ ## Overview
6
+
7
+ - No external dependencies
8
+ - Consumed by `@poe-code/agent-spawn`
9
+ - Consumed by `process-launcher`
10
+
11
+ ## Host Factory
12
+
13
+ - `createHostRunner(options)` returns a `Runner` that launches commands with `node:child_process.spawn`.
14
+ - `options.detached` starts the child as a detached process and uses process-group kill on Unix.
15
+ - `hostExecutionEnvFactory` implements the shared execution environment contract for local host execution.
16
+ - Host environments do not upload, download, detach, or reattach because the caller workspace is already local and there is no addressable remote environment.
17
+
18
+ ## Docker Factories
19
+
20
+ - `createDockerRunner(options)` returns a one-shot `Runner` that executes each command in a Docker or Podman container.
21
+ - `dockerExecutionEnvFactory` creates an addressable long-lived container environment for Poe Code runtime jobs.
22
+ - Docker environments support workspace upload/download, command execution, interactive shell, detach, attach, and cleanup.
23
+
24
+ `DockerRunnerOptions`:
25
+
26
+ - `image`: container image to run.
27
+ - `engine`: `docker` or `podman`; detected when omitted.
28
+ - `context`: Docker context name; detected when omitted.
29
+ - `mounts`: container mounts.
30
+ - `ports`: port mappings.
31
+ - `network`: Docker network.
32
+ - `extraArgs`: additional runtime arguments.
33
+ - `containerName`: optional container name prefix.
34
+
35
+ ## Environment variables
36
+
37
+ This package exposes no environment variables.
38
+
39
+ ## Configuration
40
+
41
+ This package currently exposes no package-level configuration options.
@@ -0,0 +1,2 @@
1
+ import type { DockerRunArgs } from "../types.js";
2
+ export declare function buildDockerRunArgs(input: DockerRunArgs): string[];
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ export function buildDockerRunArgs(input) {
3
+ const args = [input.engine];
4
+ if (input.engine === "docker" && input.context) {
5
+ args.push("--context", input.context);
6
+ }
7
+ args.push("run");
8
+ if (input.rm) {
9
+ args.push("--rm");
10
+ }
11
+ if (input.detached) {
12
+ args.push("-d");
13
+ }
14
+ if (input.interactive) {
15
+ args.push("-i");
16
+ }
17
+ if (input.tty) {
18
+ args.push("-t");
19
+ }
20
+ args.push("--name", input.containerName);
21
+ if (input.cwd !== undefined) {
22
+ args.push("-w", input.cwd);
23
+ }
24
+ for (const [key, value] of Object.entries(input.env ?? {})) {
25
+ args.push("-e", `${key}=${value}`);
26
+ }
27
+ for (const mount of input.mounts) {
28
+ const volume = `${path.resolve(mount.source)}:${mount.target}${mount.readonly ? ":ro" : ""}`;
29
+ args.push("-v", volume);
30
+ }
31
+ for (const port of input.ports) {
32
+ const mapping = `${port.host}:${port.container}${port.protocol === undefined || port.protocol === "tcp" ? "" : `/${port.protocol}`}`;
33
+ args.push("-p", mapping);
34
+ }
35
+ if (input.network !== undefined) {
36
+ args.push("--network", input.network);
37
+ }
38
+ args.push(...input.extraArgs, input.image, input.command, ...input.args);
39
+ return args;
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { Engine } from "../types.js";
2
+ export declare function detectContext(): string | null;
3
+ export declare function buildContextArgs(engine: Engine, context: string | null): string[];
@@ -0,0 +1,30 @@
1
+ import { execSync } from "node:child_process";
2
+ export function detectContext() {
3
+ try {
4
+ const output = execSync("colima list --json", {
5
+ encoding: "utf-8",
6
+ stdio: ["pipe", "pipe", "ignore"]
7
+ });
8
+ const lines = output.trim().split("\n").filter(Boolean);
9
+ for (const line of lines) {
10
+ const profile = JSON.parse(line);
11
+ if (profile.status === "Running" && profile.runtime === "docker") {
12
+ const name = profile.name ?? profile.profile;
13
+ if (!name) {
14
+ continue;
15
+ }
16
+ return name === "default" ? "colima" : `colima-${name}`;
17
+ }
18
+ }
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ return null;
24
+ }
25
+ export function buildContextArgs(engine, context) {
26
+ if (engine === "docker" && context) {
27
+ return ["--context", context];
28
+ }
29
+ return [];
30
+ }
@@ -0,0 +1,28 @@
1
+ import type { DockerMount, Engine, ExecutionState, ExecutionEnvFactory, Runner } from "../types.js";
2
+ interface DockerRuntime {
3
+ type: "docker";
4
+ image?: string;
5
+ dockerfile?: string;
6
+ build_context?: string;
7
+ build_args?: Record<string, string>;
8
+ mounts?: DockerMount[];
9
+ engine?: Engine;
10
+ network?: string;
11
+ extra_args?: string[];
12
+ }
13
+ export interface BuildDockerRuntimeTemplateInput {
14
+ cwd: string;
15
+ runtime: DockerRuntime;
16
+ state?: ExecutionState;
17
+ runner?: Runner;
18
+ force?: boolean;
19
+ }
20
+ export interface BuildDockerRuntimeTemplateResult {
21
+ backend: "docker";
22
+ hash: string;
23
+ image: string;
24
+ cached: boolean;
25
+ }
26
+ export declare const dockerExecutionEnvFactory: ExecutionEnvFactory;
27
+ export declare function buildDockerRuntimeTemplate(input: BuildDockerRuntimeTemplateInput): Promise<BuildDockerRuntimeTemplateResult>;
28
+ export {};
@@ -0,0 +1,428 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { buildDockerRunArgs } from "./args.js";
7
+ import { buildContextArgs, detectContext } from "./context.js";
8
+ import { detectEngine } from "./engine.js";
9
+ import { createHostRunner } from "../host/host-runner.js";
10
+ const containerCommand = ["sh", "-c", "while :; do sleep 3600; done"];
11
+ export const dockerExecutionEnvFactory = {
12
+ type: "docker",
13
+ supportsDetach: true,
14
+ async open(spec) {
15
+ const runtime = parseDockerRuntime(spec.runtime);
16
+ const runner = spec.hostRunner ?? createHostRunner();
17
+ const engine = runtime.engine ?? detectEngine();
18
+ const context = detectContext();
19
+ const image = await resolveImage({
20
+ spec,
21
+ runtime,
22
+ runner,
23
+ engine,
24
+ context
25
+ });
26
+ const containerName = createContainerName();
27
+ const runArgs = buildDockerRunArgs({
28
+ engine,
29
+ context,
30
+ image,
31
+ command: containerCommand[0],
32
+ args: containerCommand.slice(1),
33
+ cwd: undefined,
34
+ env: undefined,
35
+ mounts: runtime.mounts ?? [],
36
+ ports: [],
37
+ network: runtime.network,
38
+ containerName,
39
+ detached: true,
40
+ interactive: true,
41
+ tty: false,
42
+ rm: false,
43
+ extraArgs: runtime.extra_args ?? []
44
+ });
45
+ const [command, ...args] = runArgs;
46
+ const id = (await runAndRead(runner, { command, args, stdout: "pipe", stderr: "pipe" })).trim();
47
+ return createDockerEnv({
48
+ id,
49
+ spec,
50
+ runner,
51
+ engine,
52
+ context
53
+ });
54
+ },
55
+ async attach(envId, context) {
56
+ const engine = detectEngine();
57
+ return createDockerEnv({
58
+ id: envId,
59
+ spec: createAttachedSpec(context?.cwd),
60
+ runner: createHostRunner(),
61
+ engine,
62
+ context: detectContext(),
63
+ attachedJobId: context?.jobId
64
+ });
65
+ }
66
+ };
67
+ function createDockerEnv(input) {
68
+ const containerRef = input.id;
69
+ return {
70
+ id: containerRef,
71
+ job: input.attachedJobId === undefined
72
+ ? null
73
+ : createContainerJob(containerRef, input.runner, input.engine, input.context, input.attachedJobId),
74
+ async uploadWorkspace() {
75
+ const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-upload-"));
76
+ const archivePath = path.join(tempDir, "workspace.tar");
77
+ try {
78
+ const excludeArgs = input.spec.uploadIgnoreFiles.flatMap((ignored) => [
79
+ "--exclude",
80
+ ignored
81
+ ]);
82
+ const tarArgs = [...excludeArgs, "-cf", archivePath, "-C", input.spec.cwd, "."];
83
+ await runOrThrow(input.runner, {
84
+ command: "tar",
85
+ args: tarArgs,
86
+ stdout: "pipe",
87
+ stderr: "pipe"
88
+ });
89
+ await runOrThrow(input.runner, {
90
+ command: input.engine,
91
+ args: [
92
+ ...buildContextArgs(input.engine, input.context),
93
+ "cp",
94
+ archivePath,
95
+ `${containerRef}:/tmp/poe-workspace-upload.tar`
96
+ ],
97
+ stdout: "pipe",
98
+ stderr: "pipe"
99
+ });
100
+ await runOrThrow(input.runner, {
101
+ command: input.engine,
102
+ args: [
103
+ ...buildContextArgs(input.engine, input.context),
104
+ "exec",
105
+ containerRef,
106
+ "sh",
107
+ "-c",
108
+ `mkdir -p ${shellQuote(input.spec.cwd)} && tar -xf /tmp/poe-workspace-upload.tar -C ${shellQuote(input.spec.cwd)}`
109
+ ],
110
+ stdout: "pipe",
111
+ stderr: "pipe"
112
+ });
113
+ return { files: 0, bytes: 0, skipped: [] };
114
+ }
115
+ finally {
116
+ rmSync(tempDir, { recursive: true, force: true });
117
+ }
118
+ },
119
+ async downloadWorkspace(opts) {
120
+ const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-download-"));
121
+ const archivePath = path.join(tempDir, "workspace.tar");
122
+ try {
123
+ await runOrThrow(input.runner, {
124
+ command: input.engine,
125
+ args: [
126
+ ...buildContextArgs(input.engine, input.context),
127
+ "exec",
128
+ containerRef,
129
+ "sh",
130
+ "-c",
131
+ `tar -cf /tmp/poe-workspace-download.tar -C ${shellQuote(input.spec.cwd)} .`
132
+ ],
133
+ stdout: "pipe",
134
+ stderr: "pipe"
135
+ });
136
+ await runOrThrow(input.runner, {
137
+ command: input.engine,
138
+ args: [
139
+ ...buildContextArgs(input.engine, input.context),
140
+ "cp",
141
+ `${containerRef}:/tmp/poe-workspace-download.tar`,
142
+ archivePath
143
+ ],
144
+ stdout: "pipe",
145
+ stderr: "pipe"
146
+ });
147
+ const extractMode = opts.conflictPolicy === "refuse" ? "-xkf" : "-xf";
148
+ await runOrThrow(input.runner, {
149
+ command: "tar",
150
+ args: [extractMode, archivePath, "-C", input.spec.cwd],
151
+ stdout: "pipe",
152
+ stderr: "pipe"
153
+ });
154
+ return { files: 0, bytes: 0, conflicts: [] };
155
+ }
156
+ finally {
157
+ rmSync(tempDir, { recursive: true, force: true });
158
+ }
159
+ },
160
+ exec(spec) {
161
+ return input.runner.exec({
162
+ command: input.engine,
163
+ args: [
164
+ ...buildContextArgs(input.engine, input.context),
165
+ "exec",
166
+ ...(spec.stdin === "pipe" || spec.stdin === "inherit" ? ["-i"] : []),
167
+ ...(spec.tty === true ? ["-t"] : []),
168
+ ...(spec.cwd !== undefined ? ["-w", spec.cwd] : []),
169
+ ...buildEnvArgs(spec.env),
170
+ containerRef,
171
+ spec.command,
172
+ ...(spec.args ?? [])
173
+ ],
174
+ stdin: spec.stdin,
175
+ stdout: spec.stdout,
176
+ stderr: spec.stderr,
177
+ tty: spec.tty
178
+ });
179
+ },
180
+ async detach() {
181
+ return createContainerJob(containerRef, input.runner, input.engine, input.context);
182
+ },
183
+ shell() {
184
+ const shellSpec = input.spec.shellSpec;
185
+ return this.exec({
186
+ command: shellSpec?.command ?? input.spec.env.SHELL ?? "sh",
187
+ ...(shellSpec?.args ? { args: shellSpec.args } : {}),
188
+ cwd: input.spec.cwd,
189
+ env: shellSpec && "env" in shellSpec ? shellSpec.env : input.spec.env,
190
+ stdin: "inherit",
191
+ stdout: "inherit",
192
+ stderr: "inherit",
193
+ tty: true
194
+ });
195
+ },
196
+ async close() {
197
+ await runOrThrow(input.runner, {
198
+ command: input.engine,
199
+ args: [...buildContextArgs(input.engine, input.context), "rm", "-f", containerRef],
200
+ stdout: "pipe",
201
+ stderr: "pipe"
202
+ });
203
+ }
204
+ };
205
+ }
206
+ async function resolveImage(input) {
207
+ if (input.runtime.image !== undefined) {
208
+ return input.runtime.image;
209
+ }
210
+ const result = await buildDockerRuntimeTemplate({
211
+ cwd: input.spec.cwd,
212
+ runtime: input.runtime,
213
+ state: input.spec.state,
214
+ runner: input.runner
215
+ });
216
+ return result.image;
217
+ }
218
+ export async function buildDockerRuntimeTemplate(input) {
219
+ const runner = input.runner ?? createHostRunner();
220
+ const engine = input.runtime.engine ?? detectEngine();
221
+ const context = detectContext();
222
+ const dockerfilePath = path.resolve(input.cwd, input.runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
223
+ const buildContext = path.resolve(input.cwd, input.runtime.build_context ?? ".");
224
+ const dockerfileBytes = await readFile(dockerfilePath);
225
+ const hash = hashDockerTemplate(dockerfileBytes, input.runtime.build_args ?? {});
226
+ const cached = input.force ? null : await input.state?.templates.get("docker", hash);
227
+ if (cached?.image !== undefined) {
228
+ return {
229
+ backend: "docker",
230
+ hash,
231
+ image: cached.image,
232
+ cached: true
233
+ };
234
+ }
235
+ const image = `poe-code/local:${hash}`;
236
+ await buildImage({
237
+ runner,
238
+ engine,
239
+ context,
240
+ image,
241
+ dockerfilePath,
242
+ buildContext,
243
+ buildArgs: input.runtime.build_args ?? {}
244
+ });
245
+ await input.state?.templates.put("docker", {
246
+ hash,
247
+ image,
248
+ runtime_type: "docker",
249
+ dockerfile_path: dockerfilePath,
250
+ built_at: new Date().toISOString()
251
+ });
252
+ return {
253
+ backend: "docker",
254
+ hash,
255
+ image,
256
+ cached: false
257
+ };
258
+ }
259
+ function hashDockerTemplate(dockerfileBytes, buildArgs) {
260
+ const hash = createHash("sha256");
261
+ hash.update(dockerfileBytes);
262
+ hash.update("\0");
263
+ for (const [key, value] of sortedBuildArgs(buildArgs)) {
264
+ hash.update(key);
265
+ hash.update("=");
266
+ hash.update(value);
267
+ hash.update("\0");
268
+ }
269
+ return hash.digest("hex");
270
+ }
271
+ async function buildImage(input) {
272
+ await runOrThrow(input.runner, {
273
+ command: input.engine,
274
+ args: [
275
+ ...buildContextArgs(input.engine, input.context),
276
+ "build",
277
+ "--tag",
278
+ input.image,
279
+ "-f",
280
+ input.dockerfilePath,
281
+ ...sortedBuildArgs(input.buildArgs).flatMap(([key, value]) => [
282
+ "--build-arg",
283
+ `${key}=${value}`
284
+ ]),
285
+ input.buildContext
286
+ ],
287
+ stdout: "pipe",
288
+ stderr: "pipe"
289
+ });
290
+ }
291
+ function parseDockerRuntime(runtime) {
292
+ if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
293
+ throw new Error("docker runtime must be an object");
294
+ }
295
+ const record = runtime;
296
+ if (record.type !== "docker") {
297
+ throw new Error('docker runtime type must be "docker"');
298
+ }
299
+ return record;
300
+ }
301
+ async function runAndRead(runner, spec) {
302
+ const handle = runner.exec(spec);
303
+ const stdout = readStream(handle.stdout);
304
+ const stderr = readStream(handle.stderr);
305
+ const result = await handle.result;
306
+ const output = await stdout;
307
+ if (result.exitCode !== 0) {
308
+ const errorOutput = await stderr;
309
+ throw new Error(`Command failed with exit code ${result.exitCode}: ${spec.command} ${(spec.args ?? []).join(" ")}${errorOutput ? `\n${errorOutput}` : ""}`);
310
+ }
311
+ return output;
312
+ }
313
+ async function runOrThrow(runner, spec) {
314
+ await runAndRead(runner, spec);
315
+ }
316
+ async function readStream(stream) {
317
+ if (stream === null) {
318
+ return "";
319
+ }
320
+ stream.setEncoding("utf8");
321
+ const chunks = [];
322
+ for await (const chunk of stream) {
323
+ chunks.push(String(chunk));
324
+ }
325
+ return chunks.join("");
326
+ }
327
+ function sortedBuildArgs(buildArgs) {
328
+ return Object.entries(buildArgs).sort(([left], [right]) => left.localeCompare(right));
329
+ }
330
+ function buildEnvArgs(env) {
331
+ if (env === undefined) {
332
+ return [];
333
+ }
334
+ return Object.entries(env).flatMap(([key, value]) => ["-e", `${key}=${value}`]);
335
+ }
336
+ function createContainerName() {
337
+ return `poe-env-${randomBytes(6).toString("hex")}`;
338
+ }
339
+ function createContainerJob(containerId, runner, engine, context, jobId = containerId) {
340
+ return {
341
+ id: jobId,
342
+ envId: containerId,
343
+ tool: "docker",
344
+ argv: ["attach", containerId],
345
+ async status() {
346
+ const handle = runner.exec({
347
+ command: engine,
348
+ args: [
349
+ ...buildContextArgs(engine, context),
350
+ "inspect",
351
+ "-f",
352
+ "{{.State.Status}}",
353
+ containerId
354
+ ],
355
+ stdout: "pipe",
356
+ stderr: "pipe"
357
+ });
358
+ const stdout = await readStream(handle.stdout);
359
+ const result = await handle.result;
360
+ if (result.exitCode !== 0) {
361
+ return "lost";
362
+ }
363
+ return stdout.trim() === "running" ? "running" : "exited";
364
+ },
365
+ async *stream(opts) {
366
+ const handle = runner.exec({
367
+ command: engine,
368
+ args: [
369
+ ...buildContextArgs(engine, context),
370
+ "exec",
371
+ containerId,
372
+ "sh",
373
+ "-c",
374
+ `test -f ${shellQuote(`/tmp/poe-jobs/${jobId}.log`)} && tail -c +${(opts?.sinceByte ?? 0) + 1} ${shellQuote(`/tmp/poe-jobs/${jobId}.log`)} || true`
375
+ ],
376
+ stdout: "pipe",
377
+ stderr: "pipe"
378
+ });
379
+ const stdout = await readStream(handle.stdout);
380
+ await handle.result;
381
+ if (stdout.length > 0) {
382
+ yield { byteOffset: opts?.sinceByte ?? 0, data: stdout };
383
+ }
384
+ },
385
+ async wait() {
386
+ const handle = runner.exec({
387
+ command: engine,
388
+ args: [...buildContextArgs(engine, context), "wait", containerId],
389
+ stdout: "pipe",
390
+ stderr: "pipe"
391
+ });
392
+ const stdout = await readStream(handle.stdout);
393
+ const result = await handle.result;
394
+ return { exitCode: Number.parseInt(stdout.trim(), 10) || result.exitCode };
395
+ },
396
+ async kill(signal) {
397
+ const args = signal === undefined || signal === "SIGTERM"
398
+ ? ["stop", containerId]
399
+ : ["kill", ...(signal === "SIGKILL" ? [] : [`--signal=${signal}`]), containerId];
400
+ await runOrThrow(runner, {
401
+ command: engine,
402
+ args: [...buildContextArgs(engine, context), ...args],
403
+ stdout: "pipe",
404
+ stderr: "pipe"
405
+ });
406
+ }
407
+ };
408
+ }
409
+ function createAttachedSpec(cwd = "/workspace") {
410
+ return {
411
+ cwd,
412
+ runtime: {
413
+ type: "docker",
414
+ image: "attached",
415
+ build_args: {},
416
+ mounts: []
417
+ },
418
+ env: {},
419
+ uploadIgnoreFiles: [],
420
+ jobLabel: {
421
+ tool: "docker",
422
+ argv: []
423
+ }
424
+ };
425
+ }
426
+ function shellQuote(value) {
427
+ return `'${value.replaceAll("'", "'\\''")}'`;
428
+ }
@@ -0,0 +1,2 @@
1
+ import type { DockerRunnerOptions, Runner } from "../types.js";
2
+ export declare function createDockerRunner(options: DockerRunnerOptions): Runner;