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.
- package/dist/cli.js +274 -160
- package/dist/renderer.d.ts +8 -2
- package/dist/renderer.js +71 -12
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +132 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +116 -7
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -2
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- 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/node_modules/@poe-code/task-list/README.md +49 -5
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.d.ts +19 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +62 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +13 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +627 -0
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +253 -41
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +7 -1
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +21 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +171 -16
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +1 -1
- package/node_modules/@poe-code/task-list/dist/open.d.ts +4 -2
- package/node_modules/@poe-code/task-list/dist/open.js +27 -3
- package/node_modules/@poe-code/task-list/dist/types.d.ts +51 -3
- package/node_modules/@poe-code/task-list/dist/types.js +25 -0
- package/node_modules/@poe-code/task-list/package.json +1 -0
- package/package.json +11 -4
|
@@ -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,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
|
+
};
|