toolcraft 0.0.23 → 0.0.24

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 (147) 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/docker-execution-env.js +244 -110
  86. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +16 -4
  87. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  88. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +16 -1
  89. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  90. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  91. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  92. package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
  93. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +57 -0
  94. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +484 -0
  95. package/node_modules/@poe-code/process-runner/package.json +1 -1
  96. package/node_modules/@poe-code/task-list/README.md +0 -2
  97. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  98. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  99. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  100. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  101. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  102. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  103. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  104. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  105. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  106. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  107. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  108. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  109. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  110. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  111. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  112. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  113. package/node_modules/@poe-code/task-list/package.json +1 -2
  114. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  115. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +7 -0
  116. package/node_modules/auth-store/dist/encrypted-file-store.js +69 -7
  117. package/node_modules/auth-store/dist/index.d.ts +1 -1
  118. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  119. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  120. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  121. package/node_modules/auth-store/dist/provider-store.js +55 -7
  122. package/node_modules/auth-store/dist/types.d.ts +3 -1
  123. package/node_modules/auth-store/package.json +2 -1
  124. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  125. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  126. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  127. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  128. package/node_modules/mcp-oauth/package.json +1 -0
  129. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  130. package/node_modules/tiny-mcp-client/dist/internal.d.ts +8 -4
  131. package/node_modules/tiny-mcp-client/dist/internal.js +237 -67
  132. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  133. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  134. package/node_modules/tiny-mcp-client/package.json +2 -1
  135. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  136. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  137. package/node_modules/tiny-mcp-client/src/internal.ts +279 -77
  138. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  139. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  140. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  141. package/package.json +10 -12
  142. package/node_modules/@poe-code/file-lock/README.md +0 -52
  143. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  144. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  145. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  146. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  147. package/node_modules/@poe-code/file-lock/package.json +0 -23
@@ -2,13 +2,19 @@ import { color } from "../components/color.js";
2
2
  import { symbols } from "../components/symbols.js";
3
3
  import { resolveOutputFormat } from "../internal/output-format.js";
4
4
  import { getTheme } from "../internal/theme-detect.js";
5
+ function renderMarkdownInline(value) {
6
+ return value.replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
7
+ }
5
8
  export function renderMenu(opts) {
6
9
  const format = resolveOutputFormat();
7
10
  const selectedIndex = opts.selectedIndex ?? 0;
11
+ if (!Number.isInteger(selectedIndex) || !Number.isFinite(selectedIndex)) {
12
+ throw new Error("selectedIndex must be a finite integer.");
13
+ }
8
14
  if (format === "markdown") {
9
15
  return [
10
- `**${opts.message}**`,
11
- ...opts.options.map((option, index) => `- [${index === selectedIndex ? "x" : " "}] ${option.label}`)
16
+ `**${renderMarkdownInline(opts.message)}**`,
17
+ ...opts.options.map((option, index) => `- [${index === selectedIndex ? "x" : " "}] ${renderMarkdownInline(option.label)}`)
12
18
  ].join("\n");
13
19
  }
14
20
  if (format === "json") {
@@ -1,7 +1,7 @@
1
1
  import { color } from "../components/color.js";
2
2
  import { symbols } from "../components/symbols.js";
3
3
  import { resolveOutputFormat } from "../internal/output-format.js";
4
- export const SPINNER_FRAMES = ["◒", "◐", "◓", "◑"];
4
+ export const SPINNER_FRAMES = Object.freeze(["◒", "◐", "◓", "◑"]);
5
5
  export function renderSpinnerFrame(options) {
6
6
  const format = resolveOutputFormat();
7
7
  if (format === "markdown") {
@@ -16,22 +16,28 @@ export function renderSpinnerFrame(options) {
16
16
  })}\n`;
17
17
  }
18
18
  const frame = options.frame ?? 0;
19
- const spinnerChar = color.magenta(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
19
+ const index = ((frame % SPINNER_FRAMES.length) + SPINNER_FRAMES.length) % SPINNER_FRAMES.length;
20
+ const spinnerChar = color.magenta(SPINNER_FRAMES[index]);
20
21
  const timerSuffix = options.timer ? color.dim(` [${options.timer}]`) : "";
21
22
  const bar = color.gray(symbols.bar);
22
23
  return `${spinnerChar} ${options.message}${timerSuffix}\n${bar}`;
23
24
  }
25
+ function renderMarkdownInline(value) {
26
+ return value.replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
27
+ }
24
28
  export function renderSpinnerStopped(options) {
25
29
  const format = resolveOutputFormat();
26
30
  if (format === "markdown") {
27
- return `- ${options.message}${options.timer ? ` [${options.timer}]` : ""}\n`;
31
+ return `- ${renderMarkdownInline(options.message)}${options.timer ? ` [${renderMarkdownInline(options.timer)}]` : ""}\n`;
28
32
  }
29
33
  if (format === "json") {
30
34
  return `${JSON.stringify({
31
35
  type: "spinner",
32
36
  state: "stopped",
33
37
  message: options.message,
34
- ...(options.timer ? { timer: options.timer } : {})
38
+ code: options.code ?? 0,
39
+ ...(options.timer ? { timer: options.timer } : {}),
40
+ ...(options.subtext ? { subtext: options.subtext } : {})
35
41
  })}\n`;
36
42
  }
37
43
  const code = options.code ?? 0;
@@ -32,7 +32,12 @@ class YamlSubsetParser {
32
32
  throw new FrontmatterParseError("Invalid mapping entry.");
33
33
  }
34
34
  this.position += 1;
35
- result[entry.key] = this.readEntryValue(entry, expectedIndent);
35
+ Object.defineProperty(result, entry.key, {
36
+ configurable: true,
37
+ enumerable: true,
38
+ value: this.readEntryValue(entry, expectedIndent),
39
+ writable: true
40
+ });
36
41
  }
37
42
  return result;
38
43
  }
@@ -345,8 +350,10 @@ function sliceFrontmatterBlock(content, start, end) {
345
350
  function startsWithFrontmatterFence(value) {
346
351
  return (value.startsWith("---\n") ||
347
352
  value.startsWith("---\r\n") ||
353
+ value.startsWith("---\r") ||
348
354
  value.startsWith("\uFEFF---\n") ||
349
- value.startsWith("\uFEFF---\r\n"));
355
+ value.startsWith("\uFEFF---\r\n") ||
356
+ value.startsWith("\uFEFF---\r"));
350
357
  }
351
358
  function stripBom(value) {
352
359
  return value.startsWith("\uFEFF") ? value.slice(1) : value;
@@ -6,7 +6,11 @@ import { typography } from "../tokens/typography.js";
6
6
  import { widths } from "../tokens/widths.js";
7
7
  const lineChar = "─";
8
8
  export function render(ast, options = {}) {
9
- const width = Math.max(1, options.width ?? process.stdout.columns ?? widths.maxLine);
9
+ const requestedWidth = options.width ?? process.stdout.columns ?? widths.maxLine;
10
+ if (!Number.isFinite(requestedWidth) || requestedWidth <= 0) {
11
+ throw new Error("width must be a positive finite number.");
12
+ }
13
+ const width = Math.max(1, requestedWidth);
10
14
  const context = {
11
15
  width,
12
16
  showFrontmatter: options.showFrontmatter ?? false,
@@ -215,7 +219,20 @@ function formatFrontmatterValue(value) {
215
219
  if (typeof value === "number" || typeof value === "boolean") {
216
220
  return String(value);
217
221
  }
218
- return JSON.stringify(value);
222
+ const ancestors = [];
223
+ return JSON.stringify(value, function (_key, nestedValue) {
224
+ if (typeof nestedValue !== "object" || nestedValue === null) {
225
+ return nestedValue;
226
+ }
227
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
228
+ ancestors.pop();
229
+ }
230
+ if (ancestors.includes(nestedValue)) {
231
+ return "[Circular]";
232
+ }
233
+ ancestors.push(nestedValue);
234
+ return nestedValue;
235
+ });
219
236
  }
220
237
  function renderHtml(node, context) {
221
238
  const value = stripHtmlTags(node.value).trim();
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@poe-code/design-system",
3
3
  "version": "0.0.2",
4
+ "private": true,
4
5
  "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "scripts": {
8
- "build": "tsc",
9
+ "build": "node ../../scripts/guard-package-dist.mjs && tsc",
9
10
  "postbuild": "node scripts/smoke-built-exports.cjs",
10
11
  "lint": "cd ../.. && eslint packages/design-system --ext ts && tsc -p packages/design-system/tsconfig.json --noEmit",
11
12
  "test": "cd ../.. && vitest run $(rg --files packages/design-system/src -g '*.test.ts' | sort | tr '\\n' ' ')",
@@ -1,12 +1,13 @@
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
6
  import { buildDockerRunArgs } from "./args.js";
7
7
  import { buildContextArgs, detectContext } from "./context.js";
8
8
  import { detectEngine } from "./engine.js";
9
9
  import { createHostRunner } from "../host/host-runner.js";
10
+ import { downloadWorkspace as downloadTransferredWorkspace, uploadWorkspace as uploadTransferredWorkspace } from "../workspace-transfer.js";
10
11
  const containerCommand = ["sh", "-c", "while :; do sleep 3600; done"];
11
12
  export const dockerExecutionEnvFactory = {
12
13
  type: "docker",
@@ -53,109 +54,53 @@ export const dockerExecutionEnvFactory = {
53
54
  });
54
55
  },
55
56
  async attach(envId, context) {
56
- const engine = detectEngine();
57
+ const reattachContext = parseDockerReattachContext(context?.reattachContext);
58
+ const engine = reattachContext?.engine ?? detectEngine();
57
59
  return createDockerEnv({
58
60
  id: envId,
59
61
  spec: createAttachedSpec(context?.cwd),
60
62
  runner: createHostRunner(),
61
63
  engine,
62
- context: detectContext(),
64
+ context: reattachContext === undefined ? detectContext() : reattachContext.context,
63
65
  attachedJobId: context?.jobId
64
66
  });
65
67
  }
66
68
  };
67
69
  function createDockerEnv(input) {
68
70
  const containerRef = input.id;
71
+ const workspaceTransferEnv = {
72
+ cwd: input.spec.cwd,
73
+ uploadDir: "/tmp/poe-workspace-transfer",
74
+ workspaceDir: input.spec.cwd,
75
+ remoteFs: createContainerWorkspaceFileSystem(input)
76
+ };
77
+ let detachedJobContext = input.attachedJobId === undefined
78
+ ? null
79
+ : { id: input.attachedJobId, tool: input.spec.jobLabel.tool, argv: input.spec.jobLabel.argv };
69
80
  return {
70
81
  id: containerRef,
82
+ reattachContext: { engine: input.engine, context: input.context },
71
83
  job: input.attachedJobId === undefined
72
84
  ? null
73
- : createContainerJob(containerRef, input.runner, input.engine, input.context, input.attachedJobId),
85
+ : createContainerJob(containerRef, input.runner, input.engine, input.context, detachedJobContext),
86
+ setDetachedJobContext(context) {
87
+ detachedJobContext = context;
88
+ },
74
89
  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
- });
90
+ if (readRunnerSync(input.spec.runner) === "none") {
113
91
  return { files: 0, bytes: 0, skipped: [] };
114
92
  }
115
- finally {
116
- rmSync(tempDir, { recursive: true, force: true });
117
- }
93
+ return uploadTransferredWorkspace(workspaceTransferEnv, {
94
+ runner: readWorkspaceTransferRunner(input.spec.runner),
95
+ workspaceExclude: input.spec.uploadIgnoreFiles
96
+ });
118
97
  },
119
98
  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
- });
99
+ const sync = readRunnerSync(input.spec.runner);
100
+ if (sync === "upload" || sync === "none") {
154
101
  return { files: 0, bytes: 0, conflicts: [] };
155
102
  }
156
- finally {
157
- rmSync(tempDir, { recursive: true, force: true });
158
- }
103
+ return downloadTransferredWorkspace(workspaceTransferEnv, opts);
159
104
  },
160
105
  exec(spec) {
161
106
  return input.runner.exec({
@@ -174,18 +119,19 @@ function createDockerEnv(input) {
174
119
  stdin: spec.stdin,
175
120
  stdout: spec.stdout,
176
121
  stderr: spec.stderr,
177
- tty: spec.tty
122
+ tty: spec.tty,
123
+ signal: spec.signal
178
124
  });
179
125
  },
180
126
  async detach() {
181
- return createContainerJob(containerRef, input.runner, input.engine, input.context);
127
+ return createContainerJob(containerRef, input.runner, input.engine, input.context, detachedJobContext);
182
128
  },
183
129
  shell() {
184
130
  const shellSpec = input.spec.shellSpec;
185
131
  return this.exec({
186
132
  command: shellSpec?.command ?? input.spec.env.SHELL ?? "sh",
187
133
  ...(shellSpec?.args ? { args: shellSpec.args } : {}),
188
- cwd: input.spec.cwd,
134
+ cwd: shellSpec?.cwd ?? input.spec.cwd,
189
135
  env: shellSpec && "env" in shellSpec ? shellSpec.env : input.spec.env,
190
136
  stdin: "inherit",
191
137
  stdout: "inherit",
@@ -203,6 +149,14 @@ function createDockerEnv(input) {
203
149
  }
204
150
  };
205
151
  }
152
+ function parseDockerReattachContext(value) {
153
+ if (value !== undefined &&
154
+ (value.engine === "docker" || value.engine === "podman") &&
155
+ (value.context === null || typeof value.context === "string")) {
156
+ return { engine: value.engine, context: value.context };
157
+ }
158
+ return undefined;
159
+ }
206
160
  async function resolveImage(input) {
207
161
  if (input.runtime.image !== undefined) {
208
162
  return input.runtime.image;
@@ -222,9 +176,10 @@ export async function buildDockerRuntimeTemplate(input) {
222
176
  const dockerfilePath = path.resolve(input.cwd, input.runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
223
177
  const buildContext = path.resolve(input.cwd, input.runtime.build_context ?? ".");
224
178
  const dockerfileBytes = await readFile(dockerfilePath);
225
- const hash = hashDockerTemplate(dockerfileBytes, input.runtime.build_args ?? {});
179
+ const buildContextFiles = await readBuildContextFiles(buildContext);
180
+ const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
226
181
  const cached = input.force ? null : await input.state?.templates.get("docker", hash);
227
- if (cached?.image !== undefined) {
182
+ if (cached?.image !== undefined && (await imageExists(runner, engine, context, cached.image))) {
228
183
  return {
229
184
  backend: "docker",
230
185
  hash,
@@ -256,10 +211,18 @@ export async function buildDockerRuntimeTemplate(input) {
256
211
  cached: false
257
212
  };
258
213
  }
259
- function hashDockerTemplate(dockerfileBytes, buildArgs) {
214
+ function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engine) {
260
215
  const hash = createHash("sha256");
261
216
  hash.update(dockerfileBytes);
262
217
  hash.update("\0");
218
+ hash.update(engine);
219
+ hash.update("\0");
220
+ for (const file of buildContextFiles) {
221
+ hash.update(file.relativePath);
222
+ hash.update("\0");
223
+ hash.update(file.bytes);
224
+ hash.update("\0");
225
+ }
263
226
  for (const [key, value] of sortedBuildArgs(buildArgs)) {
264
227
  hash.update(key);
265
228
  hash.update("=");
@@ -268,6 +231,39 @@ function hashDockerTemplate(dockerfileBytes, buildArgs) {
268
231
  }
269
232
  return hash.digest("hex");
270
233
  }
234
+ async function readBuildContextFiles(buildContext) {
235
+ const files = [];
236
+ await collectBuildContextFiles(buildContext, "", files);
237
+ return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
238
+ }
239
+ async function collectBuildContextFiles(buildContext, relativeDir, files) {
240
+ const absoluteDir = path.join(buildContext, relativeDir);
241
+ const entries = await readdir(absoluteDir, { withFileTypes: true });
242
+ for (const entry of entries) {
243
+ const relativePath = path.join(relativeDir, entry.name);
244
+ if (entry.isDirectory()) {
245
+ await collectBuildContextFiles(buildContext, relativePath, files);
246
+ continue;
247
+ }
248
+ if (!entry.isFile()) {
249
+ continue;
250
+ }
251
+ files.push({
252
+ relativePath: relativePath.split(path.sep).join("/"),
253
+ bytes: await readFile(path.join(buildContext, relativePath))
254
+ });
255
+ }
256
+ }
257
+ async function imageExists(runner, engine, context, image) {
258
+ const handle = runner.exec({
259
+ command: engine,
260
+ args: [...buildContextArgs(engine, context), "image", "inspect", image],
261
+ stdout: "pipe",
262
+ stderr: "pipe"
263
+ });
264
+ const result = await handle.result;
265
+ return result.exitCode === 0;
266
+ }
271
267
  async function buildImage(input) {
272
268
  await runOrThrow(input.runner, {
273
269
  command: input.engine,
@@ -336,13 +332,108 @@ function buildEnvArgs(env) {
336
332
  function createContainerName() {
337
333
  return `poe-env-${randomBytes(6).toString("hex")}`;
338
334
  }
339
- function createContainerJob(containerId, runner, engine, context, jobId = containerId) {
335
+ function readRunnerSync(runner) {
336
+ if (typeof runner !== "object" || runner === null || !("sync" in runner)) {
337
+ return undefined;
338
+ }
339
+ const sync = runner.sync;
340
+ return sync === "both" || sync === "upload" || sync === "none" ? sync : undefined;
341
+ }
342
+ function readWorkspaceTransferRunner(runner) {
343
+ if (typeof runner !== "object" || runner === null) {
344
+ return undefined;
345
+ }
346
+ const record = runner;
347
+ const uploadMaxFileMb = typeof record.upload_max_file_mb === "number" ? record.upload_max_file_mb : undefined;
348
+ const workspace = typeof record.workspace === "object" && record.workspace !== null
349
+ && Array.isArray(record.workspace.exclude)
350
+ ? { exclude: record.workspace.exclude.filter((value) => typeof value === "string") }
351
+ : undefined;
352
+ return { ...(uploadMaxFileMb === undefined ? {} : { upload_max_file_mb: uploadMaxFileMb }), ...(workspace === undefined ? {} : { workspace }) };
353
+ }
354
+ function createContainerWorkspaceFileSystem(input) {
355
+ const execShell = (command) => runAndRead(input.runner, {
356
+ command: input.engine,
357
+ args: [...buildContextArgs(input.engine, input.context), "exec", input.id, "sh", "-c", command],
358
+ stdout: "pipe",
359
+ stderr: "pipe"
360
+ });
361
+ async function readRemoteFile(targetPath) {
362
+ const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-read-"));
363
+ const destinationPath = path.join(tempDir, "content");
364
+ try {
365
+ await runOrThrow(input.runner, {
366
+ command: input.engine,
367
+ args: [...buildContextArgs(input.engine, input.context), "cp", `${input.id}:${targetPath}`, destinationPath],
368
+ stdout: "pipe",
369
+ stderr: "pipe"
370
+ });
371
+ return await readFile(destinationPath);
372
+ }
373
+ finally {
374
+ rmSync(tempDir, { recursive: true, force: true });
375
+ }
376
+ }
377
+ async function readFileFromContainer(targetPath, encoding) {
378
+ const contents = await readRemoteFile(targetPath);
379
+ return encoding === undefined ? contents : contents.toString(encoding);
380
+ }
381
+ return {
382
+ async mkdir(targetPath) {
383
+ await execShell(`mkdir -p ${shellQuote(targetPath)}`);
384
+ },
385
+ async readdir(targetPath) {
386
+ const output = await execShell(`for item in ${shellQuote(targetPath)}/* ${shellQuote(targetPath)}/.[!.]* ${shellQuote(targetPath)}/..?*; do [ -e "$item" ] || continue; if [ -d "$item" ]; then kind=d; size=0; else kind=f; size=$(wc -c < "$item"); fi; printf '%s\\t%s\\t%s\\n' "\${item##*/}" "$kind" "$size"; done`);
387
+ return output.split("\n").filter(Boolean).map((line) => {
388
+ const [name = "", kind = "f"] = line.split("\t");
389
+ return { name, isFile: () => kind === "f", isDirectory: () => kind === "d" };
390
+ });
391
+ },
392
+ readFile: readFileFromContainer,
393
+ async writeFile(targetPath, data) {
394
+ const tempDir = mkdtempSync(path.join(tmpdir(), "poe-docker-write-"));
395
+ const sourcePath = path.join(tempDir, "content");
396
+ try {
397
+ await writeFile(sourcePath, data);
398
+ await runOrThrow(input.runner, {
399
+ command: input.engine,
400
+ args: [...buildContextArgs(input.engine, input.context), "cp", sourcePath, `${input.id}:${targetPath}`],
401
+ stdout: "pipe",
402
+ stderr: "pipe"
403
+ });
404
+ }
405
+ finally {
406
+ rmSync(tempDir, { recursive: true, force: true });
407
+ }
408
+ },
409
+ async stat(targetPath) {
410
+ 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`);
411
+ if (output.trim() === "missing") {
412
+ throw Object.assign(new Error(`ENOENT: ${targetPath}`), { code: "ENOENT" });
413
+ }
414
+ const [kind = "f", rawSize = "0"] = output.trim().split("\t");
415
+ return { size: Number(rawSize.trim()), isFile: () => kind === "f", isDirectory: () => kind === "d" };
416
+ },
417
+ async rename(oldPath, newPath) {
418
+ await execShell(`mv ${shellQuote(oldPath)} ${shellQuote(newPath)}`);
419
+ },
420
+ async rm(targetPath) {
421
+ await execShell(`rm -rf ${shellQuote(targetPath)}`);
422
+ }
423
+ };
424
+ }
425
+ function createContainerJob(containerId, runner, engine, context, detachedJobContext = null) {
426
+ const jobId = detachedJobContext?.id ?? containerId;
340
427
  return {
341
428
  id: jobId,
342
429
  envId: containerId,
343
- tool: "docker",
344
- argv: ["attach", containerId],
430
+ tool: detachedJobContext?.tool ?? "docker",
431
+ argv: detachedJobContext?.argv ?? ["attach", containerId],
345
432
  async status() {
433
+ if (detachedJobContext !== null) {
434
+ const exitCode = await readDetachedExitCode(containerId, jobId, runner, engine, context);
435
+ return exitCode === null ? "running" : "exited";
436
+ }
346
437
  const handle = runner.exec({
347
438
  command: engine,
348
439
  args: [
@@ -360,29 +451,48 @@ function createContainerJob(containerId, runner, engine, context, jobId = contai
360
451
  if (result.exitCode !== 0) {
361
452
  return "lost";
362
453
  }
363
- return stdout.trim() === "running" ? "running" : "exited";
454
+ return stdout.trim() === "exited" ? "exited" : "running";
364
455
  },
365
456
  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 };
457
+ const logFile = shellQuote(`/tmp/poe-jobs/${jobId}.log`);
458
+ const sinceCondition = opts?.since === undefined
459
+ ? ""
460
+ : ` && test $(stat -c %Y ${logFile} 2>/dev/null || stat -f %m ${logFile}) -ge ${Math.ceil(opts.since.getTime() / 1000)}`;
461
+ let byteOffset = opts?.sinceByte ?? 0;
462
+ while (true) {
463
+ const stdout = await runAndRead(runner, {
464
+ command: engine,
465
+ args: [
466
+ ...buildContextArgs(engine, context),
467
+ "exec",
468
+ containerId,
469
+ "sh",
470
+ "-c",
471
+ `test -f ${logFile}${sinceCondition} && tail -c +${byteOffset + 1} ${logFile} || true`
472
+ ],
473
+ stdout: "pipe",
474
+ stderr: "pipe"
475
+ });
476
+ if (stdout.length > 0) {
477
+ yield { byteOffset, data: stdout };
478
+ byteOffset += Buffer.byteLength(stdout);
479
+ }
480
+ if (opts?.follow !== true || (await this.status()) !== "running") {
481
+ return;
482
+ }
483
+ await new Promise((resolve) => setTimeout(resolve, 250));
383
484
  }
384
485
  },
385
486
  async wait() {
487
+ if (detachedJobContext !== null) {
488
+ while (true) {
489
+ const exitCode = await readDetachedExitCode(containerId, jobId, runner, engine, context);
490
+ if (exitCode !== null) {
491
+ return { exitCode };
492
+ }
493
+ await new Promise((resolve) => setTimeout(resolve, 25));
494
+ }
495
+ }
386
496
  const handle = runner.exec({
387
497
  command: engine,
388
498
  args: [...buildContextArgs(engine, context), "wait", containerId],
@@ -391,7 +501,8 @@ function createContainerJob(containerId, runner, engine, context, jobId = contai
391
501
  });
392
502
  const stdout = await readStream(handle.stdout);
393
503
  const result = await handle.result;
394
- return { exitCode: Number.parseInt(stdout.trim(), 10) || result.exitCode };
504
+ const exitCode = Number.parseInt(stdout.trim(), 10);
505
+ return { exitCode: Number.isNaN(exitCode) ? result.exitCode : exitCode };
395
506
  },
396
507
  async kill(signal) {
397
508
  const args = signal === undefined || signal === "SIGTERM"
@@ -406,6 +517,29 @@ function createContainerJob(containerId, runner, engine, context, jobId = contai
406
517
  }
407
518
  };
408
519
  }
520
+ async function readDetachedExitCode(containerId, jobId, runner, engine, context) {
521
+ const exitFile = shellQuote(`/tmp/poe-jobs/${jobId}.exit`);
522
+ const handle = runner.exec({
523
+ command: engine,
524
+ args: [
525
+ ...buildContextArgs(engine, context),
526
+ "exec",
527
+ containerId,
528
+ "sh",
529
+ "-c",
530
+ `test -f ${exitFile} && cat ${exitFile} || true`
531
+ ],
532
+ stdout: "pipe",
533
+ stderr: "pipe"
534
+ });
535
+ const stdout = await readStream(handle.stdout);
536
+ const result = await handle.result;
537
+ if (result.exitCode !== 0) {
538
+ return null;
539
+ }
540
+ const exitCode = Number.parseInt(stdout.trim(), 10);
541
+ return Number.isNaN(exitCode) ? null : exitCode;
542
+ }
409
543
  function createAttachedSpec(cwd = "/workspace") {
410
544
  return {
411
545
  cwd,
@@ -9,6 +9,16 @@ export function createDockerRunner(options) {
9
9
  return {
10
10
  name: "docker",
11
11
  exec(spec) {
12
+ if (spec.signal?.aborted === true) {
13
+ return {
14
+ pid: null,
15
+ stdin: null,
16
+ stdout: null,
17
+ stderr: null,
18
+ result: Promise.resolve({ exitCode: 1 }),
19
+ kill() { }
20
+ };
21
+ }
12
22
  const stdinMode = spec.stdin ?? "ignore";
13
23
  const stdoutMode = spec.stdout ?? "pipe";
14
24
  const stderrMode = spec.stderr ?? "pipe";
@@ -44,9 +54,6 @@ export function createDockerRunner(options) {
44
54
  const result = new Promise((resolve) => {
45
55
  resolveResult = resolve;
46
56
  });
47
- const cleanupAbort = bindAbortSignal(spec.signal, () => {
48
- spawnControlCommand(engine, context, ["stop", containerName]);
49
- });
50
57
  const settleResult = (exitCode) => {
51
58
  if (isResultSettled) {
52
59
  return;
@@ -55,6 +62,10 @@ export function createDockerRunner(options) {
55
62
  cleanupAbort();
56
63
  resolveResult?.({ exitCode });
57
64
  };
65
+ const cleanupAbort = bindAbortSignal(spec.signal, () => {
66
+ settleResult(1);
67
+ spawnControlCommand(engine, context, ["stop", containerName]);
68
+ });
58
69
  child.once("error", () => {
59
70
  settleResult(1);
60
71
  });
@@ -112,9 +123,10 @@ function isContainerNameCharacter(char) {
112
123
  return char === "." || char === "_" || char === "-";
113
124
  }
114
125
  function spawnControlCommand(engine, context, args) {
115
- childProcess.spawn(engine, [...buildContextArgs(engine, context), ...args], {
126
+ const child = childProcess.spawn(engine, [...buildContextArgs(engine, context), ...args], {
116
127
  stdio: "ignore"
117
128
  });
129
+ child.once("error", () => undefined);
118
130
  }
119
131
  function bindAbortSignal(signal, onAbort) {
120
132
  if (signal === undefined) {