toolcraft 0.0.23 → 0.0.25

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 (152) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +50 -13
  5. package/dist/error-report.js +32 -3
  6. package/dist/human-in-loop/approval-tasks.d.ts +1 -0
  7. package/dist/human-in-loop/approval-tasks.js +7 -5
  8. package/dist/human-in-loop/approvals-commands.js +51 -8
  9. package/dist/human-in-loop/runner.js +24 -19
  10. package/dist/human-in-loop/state-machine.d.ts +3 -3
  11. package/dist/human-in-loop/state-machine.js +13 -5
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -1
  14. package/dist/mcp-proxy.js +85 -19
  15. package/dist/mcp.compile-check.js +1 -0
  16. package/dist/mcp.d.ts +1 -0
  17. package/dist/mcp.js +50 -8
  18. package/dist/renderer.js +119 -13
  19. package/dist/sdk.compile-check.js +1 -0
  20. package/dist/sdk.d.ts +1 -0
  21. package/dist/sdk.js +56 -11
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
  24. package/node_modules/@poe-code/agent-defs/package.json +1 -1
  25. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
  26. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
  27. package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
  28. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
  29. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
  30. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
  31. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
  32. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
  33. package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
  34. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
  35. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
  38. package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
  42. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
  43. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
  44. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
  45. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
  46. package/node_modules/@poe-code/config-mutations/package.json +1 -1
  47. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  48. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  49. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  50. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  51. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  52. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  53. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  54. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  55. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  56. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  57. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  58. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  59. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  60. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  61. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  62. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  63. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  64. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  65. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  66. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  67. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  68. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  73. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  75. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  76. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  80. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  81. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  82. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  83. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  84. package/node_modules/@poe-code/design-system/package.json +2 -1
  85. package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
  86. package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
  87. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
  88. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
  89. package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
  90. package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
  91. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  92. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
  93. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  94. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  95. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  96. package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
  97. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
  98. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
  99. package/node_modules/@poe-code/process-runner/package.json +1 -1
  100. package/node_modules/@poe-code/task-list/README.md +0 -2
  101. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  102. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  103. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  104. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  105. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  106. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  107. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  108. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  109. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  110. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  111. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  112. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  113. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  114. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  115. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  116. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  117. package/node_modules/@poe-code/task-list/package.json +1 -2
  118. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  119. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
  120. package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
  121. package/node_modules/auth-store/dist/index.d.ts +1 -1
  122. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  123. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  124. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  125. package/node_modules/auth-store/dist/provider-store.js +55 -7
  126. package/node_modules/auth-store/dist/types.d.ts +3 -1
  127. package/node_modules/auth-store/package.json +2 -1
  128. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  129. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  130. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  131. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  132. package/node_modules/mcp-oauth/package.json +1 -0
  133. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  134. package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
  135. package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
  136. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  137. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  138. package/node_modules/tiny-mcp-client/package.json +2 -1
  139. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  140. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  141. package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
  142. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  144. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  145. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  146. package/package.json +10 -12
  147. package/node_modules/@poe-code/file-lock/README.md +0 -52
  148. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  149. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  150. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  151. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  152. package/node_modules/@poe-code/file-lock/package.json +0 -23
@@ -1,12 +1,14 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
3
+ import { readdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
- import { buildDockerRunArgs } from "./args.js";
6
+ import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
7
7
  import { buildContextArgs, detectContext } from "./context.js";
8
8
  import { detectEngine } from "./engine.js";
9
+ import { createDockerEnvFile } from "./env-file.js";
9
10
  import { createHostRunner } from "../host/host-runner.js";
11
+ import { downloadWorkspace as downloadTransferredWorkspace, uploadWorkspace as uploadTransferredWorkspace } from "../workspace-transfer.js";
10
12
  const containerCommand = ["sh", "-c", "while :; do sleep 3600; done"];
11
13
  export const dockerExecutionEnvFactory = {
12
14
  type: "docker",
@@ -53,144 +55,98 @@ export const dockerExecutionEnvFactory = {
53
55
  });
54
56
  },
55
57
  async attach(envId, context) {
56
- const engine = detectEngine();
58
+ const reattachContext = parseDockerReattachContext(context?.reattachContext);
59
+ const engine = reattachContext?.engine ?? detectEngine();
57
60
  return createDockerEnv({
58
61
  id: envId,
59
62
  spec: createAttachedSpec(context?.cwd),
60
63
  runner: createHostRunner(),
61
64
  engine,
62
- context: detectContext(),
65
+ context: reattachContext === undefined ? detectContext() : reattachContext.context,
63
66
  attachedJobId: context?.jobId
64
67
  });
65
68
  }
66
69
  };
67
70
  function createDockerEnv(input) {
68
71
  const containerRef = input.id;
72
+ const workspaceTransferEnv = {
73
+ cwd: input.spec.cwd,
74
+ uploadDir: "/tmp/poe-workspace-transfer",
75
+ workspaceDir: input.spec.cwd,
76
+ remoteFs: createContainerWorkspaceFileSystem(input)
77
+ };
78
+ let detachedJobContext = input.attachedJobId === undefined
79
+ ? null
80
+ : { id: input.attachedJobId, tool: input.spec.jobLabel.tool, argv: input.spec.jobLabel.argv };
69
81
  return {
70
82
  id: containerRef,
83
+ reattachContext: { engine: input.engine, context: input.context },
71
84
  job: input.attachedJobId === undefined
72
85
  ? null
73
- : createContainerJob(containerRef, input.runner, input.engine, input.context, input.attachedJobId),
86
+ : createContainerJob(containerRef, input.runner, input.engine, input.context, detachedJobContext),
87
+ setDetachedJobContext(context) {
88
+ detachedJobContext = context;
89
+ },
74
90
  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
- });
91
+ if (readRunnerSync(input.spec.runner) === "none") {
113
92
  return { files: 0, bytes: 0, skipped: [] };
114
93
  }
115
- finally {
116
- rmSync(tempDir, { recursive: true, force: true });
117
- }
94
+ return uploadTransferredWorkspace(workspaceTransferEnv, {
95
+ runner: readWorkspaceTransferRunner(input.spec.runner),
96
+ workspaceExclude: input.spec.uploadIgnoreFiles
97
+ });
118
98
  },
119
99
  async downloadWorkspace(opts) {
120
- const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-download-"));
121
- const archivePath = path.join(tempDir, "workspace.tar");
100
+ const sync = readRunnerSync(input.spec.runner);
101
+ if (sync === "upload" || sync === "none") {
102
+ return { files: 0, bytes: 0, conflicts: [] };
103
+ }
104
+ return downloadTransferredWorkspace(workspaceTransferEnv, opts);
105
+ },
106
+ exec(spec) {
107
+ const envFile = createDockerEnvFile(spec.env);
122
108
  try {
123
- await runOrThrow(input.runner, {
109
+ return cleanUpEnvFileAfterRun(input.runner.exec({
124
110
  command: input.engine,
125
111
  args: [
126
112
  ...buildContextArgs(input.engine, input.context),
127
113
  "exec",
114
+ ...(spec.stdin === "pipe" || spec.stdin === "inherit" ? ["-i"] : []),
115
+ ...(spec.tty === true ? ["-t"] : []),
116
+ ...(spec.cwd !== undefined ? ["-w", spec.cwd] : []),
117
+ ...buildDockerEnvArgs({ env: spec.env, envFilePath: envFile?.path }),
128
118
  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
119
+ spec.command,
120
+ ...(spec.args ?? [])
143
121
  ],
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: [] };
122
+ stdin: spec.stdin,
123
+ stdout: spec.stdout,
124
+ stderr: spec.stderr,
125
+ tty: spec.tty,
126
+ signal: spec.signal,
127
+ killProcessGroup: spec.killProcessGroup
128
+ }), envFile?.cleanup);
155
129
  }
156
- finally {
157
- rmSync(tempDir, { recursive: true, force: true });
130
+ catch (error) {
131
+ envFile?.cleanup();
132
+ throw error;
158
133
  }
159
134
  },
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
135
  async detach() {
181
- return createContainerJob(containerRef, input.runner, input.engine, input.context);
136
+ return createContainerJob(containerRef, input.runner, input.engine, input.context, detachedJobContext);
182
137
  },
183
138
  shell() {
184
139
  const shellSpec = input.spec.shellSpec;
185
140
  return this.exec({
186
141
  command: shellSpec?.command ?? input.spec.env.SHELL ?? "sh",
187
142
  ...(shellSpec?.args ? { args: shellSpec.args } : {}),
188
- cwd: input.spec.cwd,
143
+ cwd: shellSpec?.cwd ?? input.spec.cwd,
189
144
  env: shellSpec && "env" in shellSpec ? shellSpec.env : input.spec.env,
190
145
  stdin: "inherit",
191
146
  stdout: "inherit",
192
147
  stderr: "inherit",
193
- tty: true
148
+ tty: true,
149
+ signal: shellSpec?.signal
194
150
  });
195
151
  },
196
152
  async close() {
@@ -203,6 +159,14 @@ function createDockerEnv(input) {
203
159
  }
204
160
  };
205
161
  }
162
+ function parseDockerReattachContext(value) {
163
+ if (value !== undefined &&
164
+ (value.engine === "docker" || value.engine === "podman") &&
165
+ (value.context === null || typeof value.context === "string")) {
166
+ return { engine: value.engine, context: value.context };
167
+ }
168
+ return undefined;
169
+ }
206
170
  async function resolveImage(input) {
207
171
  if (input.runtime.image !== undefined) {
208
172
  return input.runtime.image;
@@ -222,9 +186,10 @@ export async function buildDockerRuntimeTemplate(input) {
222
186
  const dockerfilePath = path.resolve(input.cwd, input.runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
223
187
  const buildContext = path.resolve(input.cwd, input.runtime.build_context ?? ".");
224
188
  const dockerfileBytes = await readFile(dockerfilePath);
225
- const hash = hashDockerTemplate(dockerfileBytes, input.runtime.build_args ?? {});
189
+ const buildContextFiles = await readBuildContextFiles(buildContext);
190
+ const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
226
191
  const cached = input.force ? null : await input.state?.templates.get("docker", hash);
227
- if (cached?.image !== undefined) {
192
+ if (cached?.image !== undefined && (await imageExists(runner, engine, context, cached.image))) {
228
193
  return {
229
194
  backend: "docker",
230
195
  hash,
@@ -256,10 +221,18 @@ export async function buildDockerRuntimeTemplate(input) {
256
221
  cached: false
257
222
  };
258
223
  }
259
- function hashDockerTemplate(dockerfileBytes, buildArgs) {
224
+ function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engine) {
260
225
  const hash = createHash("sha256");
261
226
  hash.update(dockerfileBytes);
262
227
  hash.update("\0");
228
+ hash.update(engine);
229
+ hash.update("\0");
230
+ for (const file of buildContextFiles) {
231
+ hash.update(file.relativePath);
232
+ hash.update("\0");
233
+ hash.update(file.bytes);
234
+ hash.update("\0");
235
+ }
263
236
  for (const [key, value] of sortedBuildArgs(buildArgs)) {
264
237
  hash.update(key);
265
238
  hash.update("=");
@@ -268,6 +241,39 @@ function hashDockerTemplate(dockerfileBytes, buildArgs) {
268
241
  }
269
242
  return hash.digest("hex");
270
243
  }
244
+ async function readBuildContextFiles(buildContext) {
245
+ const files = [];
246
+ await collectBuildContextFiles(buildContext, "", files);
247
+ return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
248
+ }
249
+ async function collectBuildContextFiles(buildContext, relativeDir, files) {
250
+ const absoluteDir = path.join(buildContext, relativeDir);
251
+ const entries = await readdir(absoluteDir, { withFileTypes: true });
252
+ for (const entry of entries) {
253
+ const relativePath = path.join(relativeDir, entry.name);
254
+ if (entry.isDirectory()) {
255
+ await collectBuildContextFiles(buildContext, relativePath, files);
256
+ continue;
257
+ }
258
+ if (!entry.isFile()) {
259
+ continue;
260
+ }
261
+ files.push({
262
+ relativePath: relativePath.split(path.sep).join("/"),
263
+ bytes: await readFile(path.join(buildContext, relativePath))
264
+ });
265
+ }
266
+ }
267
+ async function imageExists(runner, engine, context, image) {
268
+ const handle = runner.exec({
269
+ command: engine,
270
+ args: [...buildContextArgs(engine, context), "image", "inspect", image],
271
+ stdout: "pipe",
272
+ stderr: "pipe"
273
+ });
274
+ const result = await handle.result;
275
+ return result.exitCode === 0;
276
+ }
271
277
  async function buildImage(input) {
272
278
  await runOrThrow(input.runner, {
273
279
  command: input.engine,
@@ -310,9 +316,49 @@ async function runAndRead(runner, spec) {
310
316
  }
311
317
  return output;
312
318
  }
319
+ async function runAndReadBytes(runner, spec) {
320
+ const handle = runner.exec(spec);
321
+ const stdout = readStreamBytes(handle.stdout);
322
+ const stderr = readStream(handle.stderr);
323
+ const result = await handle.result;
324
+ const output = await stdout;
325
+ if (result.exitCode !== 0) {
326
+ const errorOutput = await stderr;
327
+ throw new Error(`Command failed with exit code ${result.exitCode}: ${spec.command} ${(spec.args ?? []).join(" ")}${errorOutput ? `\n${errorOutput}` : ""}`);
328
+ }
329
+ return output;
330
+ }
313
331
  async function runOrThrow(runner, spec) {
314
332
  await runAndRead(runner, spec);
315
333
  }
334
+ function cleanUpEnvFileAfterRun(handle, cleanup) {
335
+ if (cleanup === undefined) {
336
+ return handle;
337
+ }
338
+ let cleanedUp = false;
339
+ const cleanupOnce = () => {
340
+ if (cleanedUp) {
341
+ return;
342
+ }
343
+ cleanedUp = true;
344
+ try {
345
+ cleanup();
346
+ }
347
+ catch {
348
+ // Cleanup is best effort; preserve the command result.
349
+ }
350
+ };
351
+ return {
352
+ pid: handle.pid,
353
+ stdin: handle.stdin,
354
+ stdout: handle.stdout,
355
+ stderr: handle.stderr,
356
+ result: handle.result.finally(cleanupOnce),
357
+ kill(signal) {
358
+ handle.kill(signal);
359
+ }
360
+ };
361
+ }
316
362
  async function readStream(stream) {
317
363
  if (stream === null) {
318
364
  return "";
@@ -324,25 +370,139 @@ async function readStream(stream) {
324
370
  }
325
371
  return chunks.join("");
326
372
  }
373
+ async function readStreamBytes(stream) {
374
+ if (stream === null) {
375
+ return Buffer.alloc(0);
376
+ }
377
+ const chunks = [];
378
+ for await (const chunk of stream) {
379
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk));
380
+ }
381
+ return Buffer.concat(chunks);
382
+ }
327
383
  function sortedBuildArgs(buildArgs) {
328
384
  return Object.entries(buildArgs).sort(([left], [right]) => left.localeCompare(right));
329
385
  }
330
- function buildEnvArgs(env) {
331
- if (env === undefined) {
332
- return [];
333
- }
334
- return Object.entries(env).flatMap(([key, value]) => ["-e", `${key}=${value}`]);
335
- }
336
386
  function createContainerName() {
337
387
  return `poe-env-${randomBytes(6).toString("hex")}`;
338
388
  }
339
- function createContainerJob(containerId, runner, engine, context, jobId = containerId) {
389
+ function readRunnerSync(runner) {
390
+ if (typeof runner !== "object" || runner === null || !("sync" in runner)) {
391
+ return undefined;
392
+ }
393
+ const sync = runner.sync;
394
+ return sync === "both" || sync === "upload" || sync === "none" ? sync : undefined;
395
+ }
396
+ function readWorkspaceTransferRunner(runner) {
397
+ if (typeof runner !== "object" || runner === null) {
398
+ return undefined;
399
+ }
400
+ const record = runner;
401
+ const uploadMaxFileMb = typeof record.upload_max_file_mb === "number" ? record.upload_max_file_mb : undefined;
402
+ const workspace = typeof record.workspace === "object" && record.workspace !== null
403
+ && Array.isArray(record.workspace.exclude)
404
+ ? { exclude: record.workspace.exclude.filter((value) => typeof value === "string") }
405
+ : undefined;
406
+ return { ...(uploadMaxFileMb === undefined ? {} : { upload_max_file_mb: uploadMaxFileMb }), ...(workspace === undefined ? {} : { workspace }) };
407
+ }
408
+ function createContainerWorkspaceFileSystem(input) {
409
+ const execShell = (command) => runAndRead(input.runner, {
410
+ command: input.engine,
411
+ args: [...buildContextArgs(input.engine, input.context), "exec", input.id, "sh", "-c", command],
412
+ stdout: "pipe",
413
+ stderr: "pipe"
414
+ });
415
+ async function readRemoteFile(targetPath) {
416
+ const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-read-"));
417
+ const destinationPath = path.join(tempDir, "content");
418
+ try {
419
+ await runOrThrow(input.runner, {
420
+ command: input.engine,
421
+ args: [...buildContextArgs(input.engine, input.context), "cp", `${input.id}:${targetPath}`, destinationPath],
422
+ stdout: "pipe",
423
+ stderr: "pipe"
424
+ });
425
+ return await readFile(destinationPath);
426
+ }
427
+ finally {
428
+ rmSync(tempDir, { recursive: true, force: true });
429
+ }
430
+ }
431
+ async function readFileFromContainer(targetPath, encoding) {
432
+ const contents = await readRemoteFile(targetPath);
433
+ return encoding === undefined ? contents : contents.toString(encoding);
434
+ }
435
+ return {
436
+ async mkdir(targetPath) {
437
+ await execShell(`mkdir -p ${shellQuote(targetPath)}`);
438
+ },
439
+ async readdir(targetPath) {
440
+ const quotedTargetPath = shellQuote(targetPath);
441
+ const output = await execShell([
442
+ `for item in ${quotedTargetPath}/* ${quotedTargetPath}/.[!.]* ${quotedTargetPath}/..?*; do`,
443
+ `[ -e "$item" ] || [ -L "$item" ] || continue;`,
444
+ `if [ -L "$item" ]; then kind=l; size=0;`,
445
+ `elif [ -d "$item" ]; then kind=d; size=0;`,
446
+ `elif [ -f "$item" ]; then kind=f; size=$(wc -c < "$item");`,
447
+ `else continue; fi;`,
448
+ `printf '%s\\t%s\\t%s\\n' "\${item##*/}" "$kind" "$size";`,
449
+ `done`
450
+ ].join(" "));
451
+ return output.split("\n").filter(Boolean).map((line) => {
452
+ const [name = "", kind = "f"] = line.split("\t");
453
+ return {
454
+ name,
455
+ isFile: () => kind === "f",
456
+ isDirectory: () => kind === "d",
457
+ isSymbolicLink: () => kind === "l"
458
+ };
459
+ });
460
+ },
461
+ readFile: readFileFromContainer,
462
+ async writeFile(targetPath, data) {
463
+ const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-write-"));
464
+ const sourcePath = path.join(tempDir, "content");
465
+ try {
466
+ await writeFile(sourcePath, data);
467
+ await runOrThrow(input.runner, {
468
+ command: input.engine,
469
+ args: [...buildContextArgs(input.engine, input.context), "cp", sourcePath, `${input.id}:${targetPath}`],
470
+ stdout: "pipe",
471
+ stderr: "pipe"
472
+ });
473
+ }
474
+ finally {
475
+ rmSync(tempDir, { recursive: true, force: true });
476
+ }
477
+ },
478
+ async stat(targetPath) {
479
+ const output = await execShell(`if [ -d ${shellQuote(targetPath)} ]; then printf 'd\\t0'; elif [ -f ${shellQuote(targetPath)} ]; then printf 'f\\t'; wc -c < ${shellQuote(targetPath)}; else printf 'missing'; fi`);
480
+ if (output.trim() === "missing") {
481
+ throw Object.assign(new Error(`ENOENT: ${targetPath}`), { code: "ENOENT" });
482
+ }
483
+ const [kind = "f", rawSize = "0"] = output.trim().split("\t");
484
+ return { size: Number(rawSize.trim()), isFile: () => kind === "f", isDirectory: () => kind === "d" };
485
+ },
486
+ async rename(oldPath, newPath) {
487
+ await execShell(`mv ${shellQuote(oldPath)} ${shellQuote(newPath)}`);
488
+ },
489
+ async rm(targetPath) {
490
+ await execShell(`rm -rf ${shellQuote(targetPath)}`);
491
+ }
492
+ };
493
+ }
494
+ function createContainerJob(containerId, runner, engine, context, detachedJobContext = null) {
495
+ const jobId = detachedJobContext?.id ?? containerId;
340
496
  return {
341
497
  id: jobId,
342
498
  envId: containerId,
343
- tool: "docker",
344
- argv: ["attach", containerId],
499
+ tool: detachedJobContext?.tool ?? "docker",
500
+ argv: detachedJobContext?.argv ?? ["attach", containerId],
345
501
  async status() {
502
+ if (detachedJobContext !== null) {
503
+ const exitCode = await readDetachedExitCode(containerId, jobId, runner, engine, context);
504
+ return exitCode === null ? "running" : "exited";
505
+ }
346
506
  const handle = runner.exec({
347
507
  command: engine,
348
508
  args: [
@@ -360,29 +520,59 @@ function createContainerJob(containerId, runner, engine, context, jobId = contai
360
520
  if (result.exitCode !== 0) {
361
521
  return "lost";
362
522
  }
363
- return stdout.trim() === "running" ? "running" : "exited";
523
+ return stdout.trim() === "exited" ? "exited" : "running";
364
524
  },
365
525
  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 };
526
+ const logFile = shellQuote(`/tmp/poe-jobs/${jobId}.log`);
527
+ const sinceCondition = opts?.since === undefined
528
+ ? ""
529
+ : ` && test $(stat -c %Y ${logFile} 2>/dev/null || stat -f %m ${logFile}) -ge ${Math.ceil(opts.since.getTime() / 1000)}`;
530
+ let byteOffset = opts?.sinceByte ?? 0;
531
+ let pendingBytes = Buffer.alloc(0);
532
+ let pendingByteOffset = byteOffset;
533
+ while (true) {
534
+ const stdout = await runAndReadBytes(runner, {
535
+ command: engine,
536
+ args: [
537
+ ...buildContextArgs(engine, context),
538
+ "exec",
539
+ containerId,
540
+ "sh",
541
+ "-c",
542
+ `test -f ${logFile}${sinceCondition} && tail -c +${byteOffset + 1} ${logFile} || true`
543
+ ],
544
+ stdout: "pipe",
545
+ stderr: "pipe"
546
+ });
547
+ if (stdout.byteLength > 0) {
548
+ const combined = pendingBytes.byteLength === 0
549
+ ? stdout
550
+ : Buffer.concat([pendingBytes, stdout]);
551
+ const completeLength = completeUtf8PrefixLength(combined);
552
+ byteOffset += stdout.byteLength;
553
+ pendingBytes = combined.subarray(completeLength);
554
+ const data = combined.subarray(0, completeLength).toString("utf8");
555
+ if (data.length > 0) {
556
+ yield { byteOffset: pendingByteOffset, data };
557
+ pendingByteOffset += completeLength;
558
+ }
559
+ }
560
+ if (opts?.follow !== true || (await this.status()) !== "running") {
561
+ return;
562
+ }
563
+ await new Promise((resolve) => setTimeout(resolve, 250));
383
564
  }
384
565
  },
385
566
  async wait() {
567
+ if (detachedJobContext !== null) {
568
+ while (true) {
569
+ const exitCode = await readDetachedExitCode(containerId, jobId, runner, engine, context);
570
+ if (exitCode !== null) {
571
+ return { exitCode };
572
+ }
573
+ await new Promise((resolve) => setTimeout(resolve, 25));
574
+ }
575
+ }
386
576
  const handle = runner.exec({
387
577
  command: engine,
388
578
  args: [...buildContextArgs(engine, context), "wait", containerId],
@@ -391,7 +581,8 @@ function createContainerJob(containerId, runner, engine, context, jobId = contai
391
581
  });
392
582
  const stdout = await readStream(handle.stdout);
393
583
  const result = await handle.result;
394
- return { exitCode: Number.parseInt(stdout.trim(), 10) || result.exitCode };
584
+ const exitCode = Number.parseInt(stdout.trim(), 10);
585
+ return { exitCode: Number.isNaN(exitCode) ? result.exitCode : exitCode };
395
586
  },
396
587
  async kill(signal) {
397
588
  const args = signal === undefined || signal === "SIGTERM"
@@ -406,6 +597,62 @@ function createContainerJob(containerId, runner, engine, context, jobId = contai
406
597
  }
407
598
  };
408
599
  }
600
+ function completeUtf8PrefixLength(contents) {
601
+ if (contents.length === 0) {
602
+ return 0;
603
+ }
604
+ let leadIndex = contents.length - 1;
605
+ while (leadIndex >= 0 && isUtf8ContinuationByte(contents[leadIndex])) {
606
+ leadIndex -= 1;
607
+ }
608
+ if (leadIndex < 0) {
609
+ return contents.length;
610
+ }
611
+ const expectedLength = utf8SequenceLength(contents[leadIndex]);
612
+ if (expectedLength === 0) {
613
+ return contents.length;
614
+ }
615
+ const availableLength = contents.length - leadIndex;
616
+ return availableLength < expectedLength ? leadIndex : contents.length;
617
+ }
618
+ function isUtf8ContinuationByte(byte) {
619
+ return byte >= 0x80 && byte <= 0xbf;
620
+ }
621
+ function utf8SequenceLength(byte) {
622
+ if (byte >= 0xc2 && byte <= 0xdf) {
623
+ return 2;
624
+ }
625
+ if (byte >= 0xe0 && byte <= 0xef) {
626
+ return 3;
627
+ }
628
+ if (byte >= 0xf0 && byte <= 0xf4) {
629
+ return 4;
630
+ }
631
+ return 0;
632
+ }
633
+ async function readDetachedExitCode(containerId, jobId, runner, engine, context) {
634
+ const exitFile = shellQuote(`/tmp/poe-jobs/${jobId}.exit`);
635
+ const handle = runner.exec({
636
+ command: engine,
637
+ args: [
638
+ ...buildContextArgs(engine, context),
639
+ "exec",
640
+ containerId,
641
+ "sh",
642
+ "-c",
643
+ `test -f ${exitFile} && cat ${exitFile} || true`
644
+ ],
645
+ stdout: "pipe",
646
+ stderr: "pipe"
647
+ });
648
+ const stdout = await readStream(handle.stdout);
649
+ const result = await handle.result;
650
+ if (result.exitCode !== 0) {
651
+ return null;
652
+ }
653
+ const exitCode = Number.parseInt(stdout.trim(), 10);
654
+ return Number.isNaN(exitCode) ? null : exitCode;
655
+ }
409
656
  function createAttachedSpec(cwd = "/workspace") {
410
657
  return {
411
658
  cwd,