@ny4n/durable 0.0.0-stage → 1.0.1

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 (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -2
  3. package/dist/documents.d.ts +66 -0
  4. package/dist/documents.d.ts.map +1 -0
  5. package/dist/documents.js +98 -0
  6. package/dist/documents.js.map +1 -0
  7. package/dist/entries.d.ts +31 -0
  8. package/dist/entries.d.ts.map +1 -0
  9. package/dist/entries.js +31 -0
  10. package/dist/entries.js.map +1 -0
  11. package/dist/env/decode.d.ts +16 -0
  12. package/dist/env/decode.d.ts.map +1 -0
  13. package/dist/env/decode.js +28 -0
  14. package/dist/env/decode.js.map +1 -0
  15. package/dist/env/index.d.ts +263 -0
  16. package/dist/env/index.d.ts.map +1 -0
  17. package/dist/env/index.js +49 -0
  18. package/dist/env/index.js.map +1 -0
  19. package/dist/env/line-scan.d.ts +13 -0
  20. package/dist/env/line-scan.d.ts.map +1 -0
  21. package/dist/env/line-scan.js +147 -0
  22. package/dist/env/line-scan.js.map +1 -0
  23. package/dist/env/node-watch.d.ts +29 -0
  24. package/dist/env/node-watch.d.ts.map +1 -0
  25. package/dist/env/node-watch.js +485 -0
  26. package/dist/env/node-watch.js.map +1 -0
  27. package/dist/env/node.d.ts +56 -0
  28. package/dist/env/node.d.ts.map +1 -0
  29. package/dist/env/node.js +1166 -0
  30. package/dist/env/node.js.map +1 -0
  31. package/dist/errors.d.ts +32 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +53 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/harness/agent.d.ts +33 -0
  36. package/dist/harness/agent.d.ts.map +1 -0
  37. package/dist/harness/agent.js +235 -0
  38. package/dist/harness/agent.js.map +1 -0
  39. package/dist/harness/assistant.d.ts +15 -0
  40. package/dist/harness/assistant.d.ts.map +1 -0
  41. package/dist/harness/assistant.js +23 -0
  42. package/dist/harness/assistant.js.map +1 -0
  43. package/dist/harness/compaction.d.ts +56 -0
  44. package/dist/harness/compaction.d.ts.map +1 -0
  45. package/dist/harness/compaction.js +384 -0
  46. package/dist/harness/compaction.js.map +1 -0
  47. package/dist/harness/context.d.ts +63 -0
  48. package/dist/harness/context.d.ts.map +1 -0
  49. package/dist/harness/context.js +221 -0
  50. package/dist/harness/context.js.map +1 -0
  51. package/dist/harness/define.d.ts +18 -0
  52. package/dist/harness/define.d.ts.map +1 -0
  53. package/dist/harness/define.js +25 -0
  54. package/dist/harness/define.js.map +1 -0
  55. package/dist/harness/events.d.ts +180 -0
  56. package/dist/harness/events.d.ts.map +1 -0
  57. package/dist/harness/events.js +370 -0
  58. package/dist/harness/events.js.map +1 -0
  59. package/dist/harness/generation.d.ts +54 -0
  60. package/dist/harness/generation.d.ts.map +1 -0
  61. package/dist/harness/generation.js +514 -0
  62. package/dist/harness/generation.js.map +1 -0
  63. package/dist/harness/harness.d.ts +17 -0
  64. package/dist/harness/harness.d.ts.map +1 -0
  65. package/dist/harness/harness.js +332 -0
  66. package/dist/harness/harness.js.map +1 -0
  67. package/dist/harness/inbox.d.ts +59 -0
  68. package/dist/harness/inbox.d.ts.map +1 -0
  69. package/dist/harness/inbox.js +97 -0
  70. package/dist/harness/inbox.js.map +1 -0
  71. package/dist/harness/json.d.ts +10 -0
  72. package/dist/harness/json.d.ts.map +1 -0
  73. package/dist/harness/json.js +33 -0
  74. package/dist/harness/json.js.map +1 -0
  75. package/dist/harness/live.d.ts +142 -0
  76. package/dist/harness/live.d.ts.map +1 -0
  77. package/dist/harness/live.js +149 -0
  78. package/dist/harness/live.js.map +1 -0
  79. package/dist/harness/output.d.ts +69 -0
  80. package/dist/harness/output.d.ts.map +1 -0
  81. package/dist/harness/output.js +318 -0
  82. package/dist/harness/output.js.map +1 -0
  83. package/dist/harness/prompt.d.ts +27 -0
  84. package/dist/harness/prompt.d.ts.map +1 -0
  85. package/dist/harness/prompt.js +131 -0
  86. package/dist/harness/prompt.js.map +1 -0
  87. package/dist/harness/provider.d.ts +14 -0
  88. package/dist/harness/provider.d.ts.map +1 -0
  89. package/dist/harness/provider.js +30 -0
  90. package/dist/harness/provider.js.map +1 -0
  91. package/dist/harness/registry.d.ts +6 -0
  92. package/dist/harness/registry.d.ts.map +1 -0
  93. package/dist/harness/registry.js +101 -0
  94. package/dist/harness/registry.js.map +1 -0
  95. package/dist/harness/run.d.ts +17 -0
  96. package/dist/harness/run.d.ts.map +1 -0
  97. package/dist/harness/run.js +26 -0
  98. package/dist/harness/run.js.map +1 -0
  99. package/dist/harness/scheduler.d.ts +106 -0
  100. package/dist/harness/scheduler.d.ts.map +1 -0
  101. package/dist/harness/scheduler.js +1743 -0
  102. package/dist/harness/scheduler.js.map +1 -0
  103. package/dist/harness/submissions.d.ts +29 -0
  104. package/dist/harness/submissions.d.ts.map +1 -0
  105. package/dist/harness/submissions.js +175 -0
  106. package/dist/harness/submissions.js.map +1 -0
  107. package/dist/harness/task-graph.d.ts +49 -0
  108. package/dist/harness/task-graph.d.ts.map +1 -0
  109. package/dist/harness/task-graph.js +168 -0
  110. package/dist/harness/task-graph.js.map +1 -0
  111. package/dist/harness/tool.d.ts +76 -0
  112. package/dist/harness/tool.d.ts.map +1 -0
  113. package/dist/harness/tool.js +736 -0
  114. package/dist/harness/tool.js.map +1 -0
  115. package/dist/harness/types.d.ts +713 -0
  116. package/dist/harness/types.d.ts.map +1 -0
  117. package/dist/harness/types.js +2 -0
  118. package/dist/harness/types.js.map +1 -0
  119. package/dist/harness/usage.d.ts +18 -0
  120. package/dist/harness/usage.d.ts.map +1 -0
  121. package/dist/harness/usage.js +57 -0
  122. package/dist/harness/usage.js.map +1 -0
  123. package/dist/harness/util.d.ts +19 -0
  124. package/dist/harness/util.d.ts.map +1 -0
  125. package/dist/harness/util.js +58 -0
  126. package/dist/harness/util.js.map +1 -0
  127. package/dist/harness/view.d.ts +47 -0
  128. package/dist/harness/view.d.ts.map +1 -0
  129. package/dist/harness/view.js +206 -0
  130. package/dist/harness/view.js.map +1 -0
  131. package/dist/ids.d.ts +6 -0
  132. package/dist/ids.d.ts.map +1 -0
  133. package/dist/ids.js +9 -0
  134. package/dist/ids.js.map +1 -0
  135. package/dist/images/cloudflare.d.ts +4 -0
  136. package/dist/images/cloudflare.d.ts.map +1 -0
  137. package/dist/images/cloudflare.js +9 -0
  138. package/dist/images/cloudflare.js.map +1 -0
  139. package/dist/images/exif.d.ts +3 -0
  140. package/dist/images/exif.d.ts.map +1 -0
  141. package/dist/images/exif.js +71 -0
  142. package/dist/images/exif.js.map +1 -0
  143. package/dist/images/index.d.ts +18 -0
  144. package/dist/images/index.d.ts.map +1 -0
  145. package/dist/images/index.js +125 -0
  146. package/dist/images/index.js.map +1 -0
  147. package/dist/images/node-worker.d.ts +17 -0
  148. package/dist/images/node-worker.d.ts.map +1 -0
  149. package/dist/images/node-worker.js +16 -0
  150. package/dist/images/node-worker.js.map +1 -0
  151. package/dist/images/node.d.ts +21 -0
  152. package/dist/images/node.d.ts.map +1 -0
  153. package/dist/images/node.js +121 -0
  154. package/dist/images/node.js.map +1 -0
  155. package/dist/index.d.ts +24 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +20 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/session/forks.d.ts +10 -0
  160. package/dist/session/forks.d.ts.map +1 -0
  161. package/dist/session/forks.js +42 -0
  162. package/dist/session/forks.js.map +1 -0
  163. package/dist/session/observation.d.ts +42 -0
  164. package/dist/session/observation.d.ts.map +1 -0
  165. package/dist/session/observation.js +281 -0
  166. package/dist/session/observation.js.map +1 -0
  167. package/dist/session/session.d.ts +133 -0
  168. package/dist/session/session.d.ts.map +1 -0
  169. package/dist/session/session.js +811 -0
  170. package/dist/session/session.js.map +1 -0
  171. package/dist/session/transaction.d.ts +119 -0
  172. package/dist/session/transaction.d.ts.map +1 -0
  173. package/dist/session/transaction.js +795 -0
  174. package/dist/session/transaction.js.map +1 -0
  175. package/dist/storage/jsonl/index.d.ts +2 -0
  176. package/dist/storage/jsonl/index.d.ts.map +1 -0
  177. package/dist/storage/jsonl/index.js +2 -0
  178. package/dist/storage/jsonl/index.js.map +1 -0
  179. package/dist/storage/jsonl/node.d.ts +6 -0
  180. package/dist/storage/jsonl/node.d.ts.map +1 -0
  181. package/dist/storage/jsonl/node.js +8 -0
  182. package/dist/storage/jsonl/node.js.map +1 -0
  183. package/dist/storage/jsonl/storage.d.ts +143 -0
  184. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  185. package/dist/storage/jsonl/storage.js +682 -0
  186. package/dist/storage/jsonl/storage.js.map +1 -0
  187. package/dist/storage/memory.d.ts +62 -0
  188. package/dist/storage/memory.d.ts.map +1 -0
  189. package/dist/storage/memory.js +733 -0
  190. package/dist/storage/memory.js.map +1 -0
  191. package/dist/storage/scan.d.ts +18 -0
  192. package/dist/storage/scan.d.ts.map +1 -0
  193. package/dist/storage/scan.js +29 -0
  194. package/dist/storage/scan.js.map +1 -0
  195. package/dist/storage/sqlite/cloudflare.d.ts +46 -0
  196. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -0
  197. package/dist/storage/sqlite/cloudflare.js +106 -0
  198. package/dist/storage/sqlite/cloudflare.js.map +1 -0
  199. package/dist/storage/sqlite/database.d.ts +37 -0
  200. package/dist/storage/sqlite/database.d.ts.map +1 -0
  201. package/dist/storage/sqlite/database.js +2 -0
  202. package/dist/storage/sqlite/database.js.map +1 -0
  203. package/dist/storage/sqlite/index.d.ts +4 -0
  204. package/dist/storage/sqlite/index.d.ts.map +1 -0
  205. package/dist/storage/sqlite/index.js +3 -0
  206. package/dist/storage/sqlite/index.js.map +1 -0
  207. package/dist/storage/sqlite/migrations.d.ts +11 -0
  208. package/dist/storage/sqlite/migrations.d.ts.map +1 -0
  209. package/dist/storage/sqlite/migrations.js +111 -0
  210. package/dist/storage/sqlite/migrations.js.map +1 -0
  211. package/dist/storage/sqlite/node.d.ts +41 -0
  212. package/dist/storage/sqlite/node.d.ts.map +1 -0
  213. package/dist/storage/sqlite/node.js +179 -0
  214. package/dist/storage/sqlite/node.js.map +1 -0
  215. package/dist/storage/sqlite/storage.d.ts +65 -0
  216. package/dist/storage/sqlite/storage.d.ts.map +1 -0
  217. package/dist/storage/sqlite/storage.js +670 -0
  218. package/dist/storage/sqlite/storage.js.map +1 -0
  219. package/dist/tasks.d.ts +6 -0
  220. package/dist/tasks.d.ts.map +1 -0
  221. package/dist/tasks.js +5 -0
  222. package/dist/tasks.js.map +1 -0
  223. package/dist/testing/assertions.d.ts +5 -0
  224. package/dist/testing/assertions.d.ts.map +1 -0
  225. package/dist/testing/assertions.js +25 -0
  226. package/dist/testing/assertions.js.map +1 -0
  227. package/dist/testing/env-conformance.d.ts +7 -0
  228. package/dist/testing/env-conformance.d.ts.map +1 -0
  229. package/dist/testing/env-conformance.js +431 -0
  230. package/dist/testing/env-conformance.js.map +1 -0
  231. package/dist/testing/index.d.ts +7 -0
  232. package/dist/testing/index.d.ts.map +1 -0
  233. package/dist/testing/index.js +6 -0
  234. package/dist/testing/index.js.map +1 -0
  235. package/dist/testing/runner.d.ts +12 -0
  236. package/dist/testing/runner.d.ts.map +1 -0
  237. package/dist/testing/runner.js +23 -0
  238. package/dist/testing/runner.js.map +1 -0
  239. package/dist/testing/storage-benchmark.d.ts +41 -0
  240. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  241. package/dist/testing/storage-benchmark.js +350 -0
  242. package/dist/testing/storage-benchmark.js.map +1 -0
  243. package/dist/testing/storage-conformance.d.ts +4 -0
  244. package/dist/testing/storage-conformance.d.ts.map +1 -0
  245. package/dist/testing/storage-conformance.js +1213 -0
  246. package/dist/testing/storage-conformance.js.map +1 -0
  247. package/dist/testing/types.d.ts +37 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing/types.js +2 -0
  250. package/dist/testing/types.js.map +1 -0
  251. package/dist/tools/bash.d.ts +55 -0
  252. package/dist/tools/bash.d.ts.map +1 -0
  253. package/dist/tools/bash.js +138 -0
  254. package/dist/tools/bash.js.map +1 -0
  255. package/dist/tools/edit-diff.d.ts +89 -0
  256. package/dist/tools/edit-diff.d.ts.map +1 -0
  257. package/dist/tools/edit-diff.js +386 -0
  258. package/dist/tools/edit-diff.js.map +1 -0
  259. package/dist/tools/edit.d.ts +18 -0
  260. package/dist/tools/edit.d.ts.map +1 -0
  261. package/dist/tools/edit.js +117 -0
  262. package/dist/tools/edit.js.map +1 -0
  263. package/dist/tools/env.d.ts +5 -0
  264. package/dist/tools/env.d.ts.map +1 -0
  265. package/dist/tools/env.js +7 -0
  266. package/dist/tools/env.js.map +1 -0
  267. package/dist/tools/file-mutation-queue.d.ts +9 -0
  268. package/dist/tools/file-mutation-queue.d.ts.map +1 -0
  269. package/dist/tools/file-mutation-queue.js +52 -0
  270. package/dist/tools/file-mutation-queue.js.map +1 -0
  271. package/dist/tools/image-processor.d.ts +47 -0
  272. package/dist/tools/image-processor.d.ts.map +1 -0
  273. package/dist/tools/image-processor.js +30 -0
  274. package/dist/tools/image-processor.js.map +1 -0
  275. package/dist/tools/image.d.ts +20 -0
  276. package/dist/tools/image.d.ts.map +1 -0
  277. package/dist/tools/image.js +178 -0
  278. package/dist/tools/image.js.map +1 -0
  279. package/dist/tools/index.d.ts +46 -0
  280. package/dist/tools/index.d.ts.map +1 -0
  281. package/dist/tools/index.js +23 -0
  282. package/dist/tools/index.js.map +1 -0
  283. package/dist/tools/path-utils.d.ts +5 -0
  284. package/dist/tools/path-utils.d.ts.map +1 -0
  285. package/dist/tools/path-utils.js +26 -0
  286. package/dist/tools/path-utils.js.map +1 -0
  287. package/dist/tools/read.d.ts +25 -0
  288. package/dist/tools/read.d.ts.map +1 -0
  289. package/dist/tools/read.js +235 -0
  290. package/dist/tools/read.js.map +1 -0
  291. package/dist/tools/write.d.ts +10 -0
  292. package/dist/tools/write.d.ts.map +1 -0
  293. package/dist/tools/write.js +31 -0
  294. package/dist/tools/write.js.map +1 -0
  295. package/dist/truncate.d.ts +64 -0
  296. package/dist/truncate.d.ts.map +1 -0
  297. package/dist/truncate.js +154 -0
  298. package/dist/truncate.js.map +1 -0
  299. package/dist/types.d.ts +932 -0
  300. package/dist/types.d.ts.map +1 -0
  301. package/dist/types.js +3 -0
  302. package/dist/types.js.map +1 -0
  303. package/package.json +140 -5
@@ -0,0 +1,1166 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants, createWriteStream } from "node:fs";
4
+ import { access, appendFile, lstat, mkdir, mkdtemp, opendir, open as openFile, readdir, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
5
+ import { homedir, constants as osConstants, tmpdir } from "node:os";
6
+ import { basename, isAbsolute, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { StreamDecoder } from "./decode.js";
9
+ import { ExecutionError, err, FileError, ok, toError, } from "./index.js";
10
+ import { LineScanner } from "./line-scan.js";
11
+ import { NodeFileWatcher } from "./node-watch.js";
12
+ const MAX_TIMEOUT_MS = 2_147_483_647;
13
+ const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
14
+ const EXIT_STDIO_GRACE_MS = 100;
15
+ const SPILL_HIGH_WATER_MARK = 1024 * 1024;
16
+ /** Largest single read of a `BinaryReader`, so a huge `length` allocates only as much as the file yields. */
17
+ const BINARY_READ_CHUNK = 1024 * 1024;
18
+ function resolveTimeoutMs(timeout) {
19
+ if (timeout === undefined)
20
+ return ok(undefined);
21
+ if (!Number.isFinite(timeout) || timeout <= 0) {
22
+ return err(new ExecutionError("timeout", "Invalid timeout: must be a finite number of seconds"));
23
+ }
24
+ const timeoutMs = timeout * 1000;
25
+ if (timeoutMs > MAX_TIMEOUT_MS) {
26
+ return err(new ExecutionError("timeout", `Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`));
27
+ }
28
+ return ok(timeoutMs);
29
+ }
30
+ function resolvePath(cwd, path) {
31
+ let normalized = path;
32
+ if (normalized === "~") {
33
+ normalized = homedir();
34
+ }
35
+ else if (normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))) {
36
+ normalized = join(homedir(), normalized.slice(2));
37
+ }
38
+ else if (normalized.startsWith("file://")) {
39
+ try {
40
+ normalized = fileURLToPath(normalized);
41
+ }
42
+ catch {
43
+ // Keep malformed URLs as ordinary paths so filesystem methods preserve their non-throwing contract.
44
+ }
45
+ }
46
+ return isAbsolute(normalized) ? resolve(normalized) : resolve(cwd, normalized);
47
+ }
48
+ function fileKindFromStats(stats) {
49
+ if (stats.isFile())
50
+ return "file";
51
+ if (stats.isDirectory())
52
+ return "directory";
53
+ if (stats.isSymbolicLink())
54
+ return "symlink";
55
+ return undefined;
56
+ }
57
+ function fileInfoFromStats(path, stats) {
58
+ const kind = fileKindFromStats(stats);
59
+ if (!kind)
60
+ return err(new FileError("invalid", "Unsupported file type", path));
61
+ return ok({
62
+ name: basename(path),
63
+ path,
64
+ kind,
65
+ size: stats.size,
66
+ mtimeMs: stats.mtimeMs,
67
+ });
68
+ }
69
+ function isNodeError(error) {
70
+ return error instanceof Error && "code" in error;
71
+ }
72
+ function toFileError(error, fallbackPath) {
73
+ if (error instanceof FileError)
74
+ return error;
75
+ const cause = toError(error);
76
+ const nodeError = isNodeError(error) ? error : undefined;
77
+ const path = typeof nodeError?.path === "string" ? nodeError.path : fallbackPath;
78
+ if (nodeError) {
79
+ const message = nodeError.message;
80
+ switch (nodeError.code) {
81
+ case "ABORT_ERR":
82
+ return new FileError("aborted", message, path, cause);
83
+ case "ENOENT":
84
+ return new FileError("not_found", message, path, cause);
85
+ case "EACCES":
86
+ case "EPERM":
87
+ return new FileError("permission_denied", message, path, cause);
88
+ case "ENOTDIR":
89
+ return new FileError("not_directory", message, path, cause);
90
+ case "EISDIR":
91
+ return new FileError("is_directory", message, path, cause);
92
+ case "EINVAL":
93
+ return new FileError("invalid", message, path, cause);
94
+ }
95
+ }
96
+ return new FileError("unknown", cause.message, path, cause);
97
+ }
98
+ function abortResult(signal, path) {
99
+ return signal?.aborted ? err(new FileError("aborted", "aborted", path)) : undefined;
100
+ }
101
+ async function pathExists(path) {
102
+ try {
103
+ await access(path, constants.F_OK);
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ async function runCommand(command, args, timeoutMs) {
111
+ return await new Promise((resolve) => {
112
+ let stdout = "";
113
+ let child;
114
+ try {
115
+ child = spawn(command, args, {
116
+ stdio: ["ignore", "pipe", "ignore"],
117
+ windowsHide: true,
118
+ });
119
+ }
120
+ catch {
121
+ resolve({ stdout: "", status: null });
122
+ return;
123
+ }
124
+ const timeout = setTimeout(() => {
125
+ if (child.pid)
126
+ killProcessTree(child.pid);
127
+ }, timeoutMs);
128
+ child.stdout?.setEncoding("utf8");
129
+ child.stdout?.on("data", (chunk) => {
130
+ stdout += chunk;
131
+ });
132
+ child.on("error", () => {
133
+ clearTimeout(timeout);
134
+ resolve({ stdout: "", status: null });
135
+ });
136
+ child.on("close", (status) => {
137
+ clearTimeout(timeout);
138
+ resolve({ stdout, status });
139
+ });
140
+ });
141
+ }
142
+ async function findBashOnPath() {
143
+ const result = process.platform === "win32"
144
+ ? await runCommand("where", ["bash.exe"], 5000)
145
+ : await runCommand("which", ["bash"], 5000);
146
+ if (result.status !== 0 || !result.stdout)
147
+ return null;
148
+ const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
149
+ return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null;
150
+ }
151
+ function isLegacyWslBashPath(path) {
152
+ const normalized = path.replace(/\//g, "\\").toLowerCase();
153
+ return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized);
154
+ }
155
+ function getBashShellConfig(shell) {
156
+ return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] };
157
+ }
158
+ async function getShellConfig(customShellPath) {
159
+ if (customShellPath) {
160
+ if (await pathExists(customShellPath)) {
161
+ return ok(getBashShellConfig(customShellPath));
162
+ }
163
+ return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`));
164
+ }
165
+ if (process.platform === "win32") {
166
+ const candidates = [];
167
+ const programFiles = process.env.ProgramFiles;
168
+ if (programFiles)
169
+ candidates.push(`${programFiles}\\Git\\bin\\bash.exe`);
170
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
171
+ if (programFilesX86)
172
+ candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
173
+ for (const candidate of candidates) {
174
+ if (await pathExists(candidate)) {
175
+ return ok(getBashShellConfig(candidate));
176
+ }
177
+ }
178
+ const bashOnPath = await findBashOnPath();
179
+ if (bashOnPath) {
180
+ return ok(getBashShellConfig(bashOnPath));
181
+ }
182
+ return err(new ExecutionError("shell_unavailable", `No bash shell found. Options:\n` +
183
+ ` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
184
+ ` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
185
+ " 3. Configure an explicit shellPath\n\n" +
186
+ `Searched Git Bash in:\n${candidates.map((path) => ` ${path}`).join("\n")}`));
187
+ }
188
+ if (await pathExists("/bin/bash")) {
189
+ return ok(getBashShellConfig("/bin/bash"));
190
+ }
191
+ const bashOnPath = await findBashOnPath();
192
+ if (bashOnPath) {
193
+ return ok(getBashShellConfig(bashOnPath));
194
+ }
195
+ return ok({ shell: "sh", args: ["-c"] });
196
+ }
197
+ function getShellEnv(baseEnv, extraEnv, inheritEnv = true) {
198
+ if (!inheritEnv)
199
+ return { ...extraEnv };
200
+ return {
201
+ ...process.env,
202
+ ...baseEnv,
203
+ ...extraEnv,
204
+ };
205
+ }
206
+ function killProcessTree(pid) {
207
+ if (process.platform === "win32") {
208
+ try {
209
+ const child = spawn(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe"), ["/F", "/T", "/PID", String(pid)], {
210
+ stdio: "ignore",
211
+ detached: true,
212
+ windowsHide: true,
213
+ });
214
+ // A failed spawn emits "error" asynchronously; consume it to avoid crashing Node.
215
+ child.once("error", () => { });
216
+ }
217
+ catch {
218
+ // Ignore errors.
219
+ }
220
+ return;
221
+ }
222
+ try {
223
+ process.kill(-pid, "SIGKILL");
224
+ }
225
+ catch {
226
+ try {
227
+ process.kill(pid, "SIGKILL");
228
+ }
229
+ catch {
230
+ // Process already dead.
231
+ }
232
+ }
233
+ }
234
+ function waitForChildProcess(child, spillIsDraining) {
235
+ return new Promise((resolvePromise, reject) => {
236
+ let settled = false;
237
+ let exited = false;
238
+ let exitCode = null;
239
+ let exitSignal = null;
240
+ let postExitTimer;
241
+ let stdoutEnded = child.stdout === null;
242
+ let stderrEnded = child.stderr === null;
243
+ const cleanup = () => {
244
+ if (postExitTimer)
245
+ clearTimeout(postExitTimer);
246
+ child.removeListener("error", onError);
247
+ child.removeListener("exit", onExit);
248
+ child.removeListener("close", onClose);
249
+ child.stdout?.removeListener("end", onStdoutEnd);
250
+ child.stderr?.removeListener("end", onStderrEnd);
251
+ child.stdout?.removeListener("data", onData);
252
+ child.stderr?.removeListener("data", onData);
253
+ };
254
+ const finalize = () => {
255
+ if (settled)
256
+ return;
257
+ settled = true;
258
+ cleanup();
259
+ child.stdout?.destroy();
260
+ child.stderr?.destroy();
261
+ resolvePromise({ code: exitCode, signal: exitSignal });
262
+ };
263
+ const maybeFinalizeAfterExit = () => {
264
+ if (exited && stdoutEnded && stderrEnded)
265
+ finalize();
266
+ };
267
+ const armIdleTimer = () => {
268
+ if (postExitTimer)
269
+ clearTimeout(postExitTimer);
270
+ postExitTimer = setTimeout(() => {
271
+ if (spillIsDraining())
272
+ armIdleTimer();
273
+ else
274
+ finalize();
275
+ }, EXIT_STDIO_GRACE_MS);
276
+ };
277
+ const onData = () => {
278
+ if (exited && !settled)
279
+ armIdleTimer();
280
+ };
281
+ const onStdoutEnd = () => {
282
+ stdoutEnded = true;
283
+ maybeFinalizeAfterExit();
284
+ };
285
+ const onStderrEnd = () => {
286
+ stderrEnded = true;
287
+ maybeFinalizeAfterExit();
288
+ };
289
+ const onError = (error) => {
290
+ if (settled)
291
+ return;
292
+ settled = true;
293
+ cleanup();
294
+ reject(error);
295
+ };
296
+ const onExit = (code, signal) => {
297
+ exited = true;
298
+ exitCode = code;
299
+ exitSignal = signal;
300
+ maybeFinalizeAfterExit();
301
+ if (!settled)
302
+ armIdleTimer();
303
+ };
304
+ const onClose = (code, signal) => {
305
+ exitCode = code;
306
+ exitSignal = signal;
307
+ finalize();
308
+ };
309
+ child.stdout?.once("end", onStdoutEnd);
310
+ child.stderr?.once("end", onStderrEnd);
311
+ child.stdout?.on("data", onData);
312
+ child.stderr?.on("data", onData);
313
+ child.once("error", onError);
314
+ child.once("exit", onExit);
315
+ child.once("close", onClose);
316
+ });
317
+ }
318
+ /** Strict LF reader; Node readline does not report whether its final line was newline-terminated. */
319
+ class NodeTextLineReader {
320
+ file;
321
+ path;
322
+ decoder = new StreamDecoder();
323
+ chunk = new Uint8Array(64 * 1024);
324
+ byteOffset = 0;
325
+ buffered = "";
326
+ ended = false;
327
+ closed = false;
328
+ constructor(file, path) {
329
+ this.file = file;
330
+ this.path = path;
331
+ }
332
+ async readLine(context) {
333
+ const aborted = abortResult(context.abortSignal, this.path);
334
+ if (aborted)
335
+ return aborted;
336
+ if (this.closed)
337
+ return err(new FileError("invalid", "Text line reader is closed", this.path));
338
+ try {
339
+ while (true) {
340
+ const newline = this.buffered.indexOf("\n");
341
+ if (newline !== -1) {
342
+ const text = this.buffered.slice(0, newline);
343
+ this.buffered = this.buffered.slice(newline + 1);
344
+ return ok({ text, terminated: true });
345
+ }
346
+ if (this.ended) {
347
+ if (this.buffered.length === 0)
348
+ return ok(undefined);
349
+ const text = this.buffered;
350
+ this.buffered = "";
351
+ return ok({ text, terminated: false });
352
+ }
353
+ // Explicit positions allow an aborted read to be retried without skipping bytes.
354
+ const { bytesRead } = await this.file.read(this.chunk, 0, this.chunk.length, this.byteOffset);
355
+ const afterReadAbort = abortResult(context.abortSignal, this.path);
356
+ if (afterReadAbort)
357
+ return afterReadAbort;
358
+ this.byteOffset += bytesRead;
359
+ if (bytesRead === 0) {
360
+ this.buffered += this.decoder.decode();
361
+ this.ended = true;
362
+ }
363
+ else {
364
+ this.buffered += this.decoder.decode(this.chunk.subarray(0, bytesRead));
365
+ }
366
+ }
367
+ }
368
+ catch (error) {
369
+ return err(toFileError(error, this.path));
370
+ }
371
+ }
372
+ async close(_context) {
373
+ if (this.closed)
374
+ return;
375
+ this.closed = true;
376
+ this.buffered = "";
377
+ try {
378
+ await this.file.close();
379
+ }
380
+ catch {
381
+ // Closing is best-effort, including after cancellation or an earlier I/O failure.
382
+ }
383
+ }
384
+ }
385
+ function closedResult(what, path) {
386
+ return err(new FileError("invalid", `${what} is closed`, path));
387
+ }
388
+ class NodeBinaryReader {
389
+ file;
390
+ path;
391
+ closed = false;
392
+ constructor(file, path) {
393
+ this.file = file;
394
+ this.path = path;
395
+ }
396
+ async info(context) {
397
+ const aborted = abortResult(context.abortSignal, this.path);
398
+ if (aborted)
399
+ return aborted;
400
+ if (this.closed)
401
+ return closedResult("Binary reader", this.path);
402
+ try {
403
+ return fileInfoFromStats(this.path, await this.file.stat());
404
+ }
405
+ catch (error) {
406
+ return err(toFileError(error, this.path));
407
+ }
408
+ }
409
+ async read(offset, length, context) {
410
+ const aborted = abortResult(context.abortSignal, this.path);
411
+ if (aborted)
412
+ return aborted;
413
+ if (this.closed)
414
+ return closedResult("Binary reader", this.path);
415
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0) {
416
+ return err(new FileError("invalid", "Offset and length must be non-negative safe integers", this.path));
417
+ }
418
+ const chunks = [];
419
+ let total = 0;
420
+ try {
421
+ while (total < length) {
422
+ const chunk = new Uint8Array(Math.min(length - total, BINARY_READ_CHUNK));
423
+ const { bytesRead } = await this.file.read(chunk, 0, chunk.length, offset + total);
424
+ const afterReadAbort = abortResult(context.abortSignal, this.path);
425
+ if (afterReadAbort)
426
+ return afterReadAbort;
427
+ if (bytesRead === 0)
428
+ break;
429
+ chunks.push(bytesRead === chunk.length ? chunk : chunk.subarray(0, bytesRead));
430
+ total += bytesRead;
431
+ }
432
+ }
433
+ catch (error) {
434
+ return err(toFileError(error, this.path));
435
+ }
436
+ if (chunks.length === 1)
437
+ return ok(chunks[0]);
438
+ const bytes = new Uint8Array(total);
439
+ let position = 0;
440
+ for (const chunk of chunks) {
441
+ bytes.set(chunk, position);
442
+ position += chunk.length;
443
+ }
444
+ return ok(bytes);
445
+ }
446
+ async scanLines(options, context) {
447
+ const aborted = abortResult(context.abortSignal, this.path);
448
+ if (aborted)
449
+ return aborted;
450
+ if (this.closed)
451
+ return closedResult("Binary reader", this.path);
452
+ let scanner;
453
+ try {
454
+ scanner = new LineScanner(options.startLine, options.endLine);
455
+ }
456
+ catch {
457
+ return err(new FileError("invalid", "Invalid line range", this.path));
458
+ }
459
+ const chunk = new Uint8Array(64 * 1024);
460
+ try {
461
+ for (let position = 0;;) {
462
+ const { bytesRead } = await this.file.read(chunk, 0, chunk.length, position);
463
+ const afterReadAbort = abortResult(context.abortSignal, this.path);
464
+ if (afterReadAbort)
465
+ return afterReadAbort;
466
+ if (bytesRead === 0)
467
+ return ok(scanner.finish());
468
+ scanner.push(chunk.subarray(0, bytesRead));
469
+ position += bytesRead;
470
+ }
471
+ }
472
+ catch (error) {
473
+ return err(toFileError(error, this.path));
474
+ }
475
+ }
476
+ async close(_context) {
477
+ if (this.closed)
478
+ return;
479
+ this.closed = true;
480
+ await this.file.close().catch(() => undefined);
481
+ }
482
+ }
483
+ class NodeDirReader {
484
+ dir;
485
+ path;
486
+ done = false;
487
+ closed = false;
488
+ constructor(dir, path) {
489
+ this.dir = dir;
490
+ this.path = path;
491
+ }
492
+ async next(maxEntries, context) {
493
+ const aborted = abortResult(context.abortSignal, this.path);
494
+ if (aborted)
495
+ return aborted;
496
+ if (this.closed)
497
+ return closedResult("Directory reader", this.path);
498
+ if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0) {
499
+ return err(new FileError("invalid", "maxEntries must be a positive safe integer", this.path));
500
+ }
501
+ const entries = [];
502
+ try {
503
+ while (!this.done && entries.length < maxEntries) {
504
+ const entry = await this.dir.read();
505
+ if (entry === null) {
506
+ this.done = true;
507
+ break;
508
+ }
509
+ const loopAbort = abortResult(context.abortSignal, this.path);
510
+ if (loopAbort)
511
+ return loopAbort;
512
+ const entryPath = resolve(this.path, entry.name);
513
+ let stats;
514
+ try {
515
+ stats = await lstat(entryPath);
516
+ }
517
+ catch (error) {
518
+ // Removed between enumeration and lstat: not part of the listing any more.
519
+ if (isNodeError(error) && error.code === "ENOENT")
520
+ continue;
521
+ return err(toFileError(error, entryPath));
522
+ }
523
+ const info = fileInfoFromStats(entryPath, stats);
524
+ if (info.ok)
525
+ entries.push(info.value);
526
+ }
527
+ return ok({ entries, done: this.done });
528
+ }
529
+ catch (error) {
530
+ return err(toFileError(error, this.path));
531
+ }
532
+ }
533
+ async close(_context) {
534
+ if (this.closed)
535
+ return;
536
+ this.closed = true;
537
+ await this.dir.close().catch(() => undefined);
538
+ }
539
+ }
540
+ function symlinkRefused(path, cause) {
541
+ return new FileError("invalid", "Refusing to follow a symbolic link", path, cause);
542
+ }
543
+ export class NodeExecutionEnv {
544
+ /** Every local environment sees the same files. */
545
+ id = "node:local";
546
+ cwd;
547
+ shellPath;
548
+ shellEnv;
549
+ watchOptions;
550
+ activeChildPids = new Set();
551
+ constructor(options) {
552
+ this.cwd = options.cwd;
553
+ this.shellPath = options.shellPath;
554
+ this.shellEnv = options.shellEnv;
555
+ this.watchOptions = options.watch ?? {};
556
+ }
557
+ async watch(targets, onChange, context) {
558
+ const aborted = abortResult(context.abortSignal);
559
+ if (aborted)
560
+ return aborted;
561
+ try {
562
+ const watcher = await NodeFileWatcher.open(targets, (path) => resolvePath(this.cwd, path), onChange, this.watchOptions);
563
+ const afterOpenAbort = abortResult(context.abortSignal);
564
+ if (afterOpenAbort) {
565
+ await watcher.close(context);
566
+ return afterOpenAbort;
567
+ }
568
+ return ok(watcher);
569
+ }
570
+ catch (error) {
571
+ return err(toFileError(error));
572
+ }
573
+ }
574
+ async absolutePath(path, _context) {
575
+ return ok(resolvePath(this.cwd, path));
576
+ }
577
+ async joinPath(parts, _context) {
578
+ return ok(join(...parts));
579
+ }
580
+ async exec(command, options, context) {
581
+ const signal = context.abortSignal;
582
+ if (signal?.aborted)
583
+ return err(new ExecutionError("aborted", "aborted"));
584
+ const timeoutMsResult = resolveTimeoutMs(options?.timeout);
585
+ if (!timeoutMsResult.ok)
586
+ return err(timeoutMsResult.error);
587
+ const timeoutMs = timeoutMsResult.value;
588
+ const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd;
589
+ // A string runs through the shell; an argv array runs its program directly, without shell parsing.
590
+ let program;
591
+ let args;
592
+ let stdinCommand;
593
+ if (typeof command === "string") {
594
+ const shellConfig = await getShellConfig(this.shellPath);
595
+ if (!shellConfig.ok)
596
+ return shellConfig;
597
+ const commandFromStdin = shellConfig.value.commandTransport === "stdin";
598
+ program = shellConfig.value.shell;
599
+ args = commandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command];
600
+ stdinCommand = commandFromStdin ? command : undefined;
601
+ }
602
+ else {
603
+ const [first, ...rest] = command;
604
+ if (first === undefined)
605
+ return err(new ExecutionError("spawn_error", "Empty argv: no program to run"));
606
+ program = first;
607
+ args = rest;
608
+ }
609
+ try {
610
+ await access(cwd, constants.F_OK);
611
+ }
612
+ catch (error) {
613
+ const cause = toError(error);
614
+ return err(new ExecutionError("spawn_error", `Working directory does not exist: ${cwd}\nCannot execute bash commands.`, cause));
615
+ }
616
+ return await new Promise((resolvePromise) => {
617
+ let settled = false;
618
+ let timedOut = false;
619
+ let callbackError;
620
+ let spillError;
621
+ let child;
622
+ let timeoutId;
623
+ const spillPrefix = [];
624
+ let spillPath;
625
+ const spillQueue = [];
626
+ let spillStart;
627
+ let spillStream;
628
+ let spillBackpressured = false;
629
+ const onAbort = () => {
630
+ if (child?.pid)
631
+ killProcessTree(child.pid);
632
+ };
633
+ const failCallback = (error) => {
634
+ if (callbackError !== undefined)
635
+ return;
636
+ const cause = toError(error);
637
+ callbackError = new ExecutionError("callback_error", cause.message, cause);
638
+ onAbort();
639
+ };
640
+ // One decoder per stream, so a character split across chunks of one stream survives interleaving.
641
+ const stdoutDecoder = new StreamDecoder();
642
+ const stderrDecoder = new StreamDecoder();
643
+ // No output reaches the caller after exec() settled, for example from a descendant holding stdio open.
644
+ const emit = (text, stream) => {
645
+ if (settled || text === "" || options?.onOutput === undefined || callbackError !== undefined)
646
+ return;
647
+ try {
648
+ options.onOutput(text, context, { stream });
649
+ }
650
+ catch (error) {
651
+ failCallback(error);
652
+ }
653
+ };
654
+ // Output seen before the spill starts: counted against the thresholds and kept for the spill's prefix.
655
+ let seenBytes = 0;
656
+ let seenNewlines = 0;
657
+ const settle = (result) => {
658
+ if (settled)
659
+ return;
660
+ settled = true;
661
+ if (timeoutId)
662
+ clearTimeout(timeoutId);
663
+ if (signal)
664
+ signal.removeEventListener("abort", onAbort);
665
+ if (child?.pid)
666
+ this.activeChildPids.delete(child.pid);
667
+ resolvePromise(result);
668
+ };
669
+ const pauseOutput = () => {
670
+ child?.stdout?.pause();
671
+ child?.stderr?.pause();
672
+ };
673
+ const resumeOutput = () => {
674
+ if (callbackError || spillError || timedOut || signal?.aborted || spillBackpressured)
675
+ return;
676
+ child?.stdout?.resume();
677
+ child?.stderr?.resume();
678
+ };
679
+ const failSpill = (error) => {
680
+ if (spillError !== undefined)
681
+ return;
682
+ const cause = toError(error);
683
+ spillError = new ExecutionError("unknown", `Failed to preserve complete shell output: ${cause.message}`, cause);
684
+ spillBackpressured = false;
685
+ onAbort();
686
+ };
687
+ const writeSpill = (chunk) => {
688
+ if (spillStream === undefined || chunk.length === 0)
689
+ return;
690
+ if (spillStream.write(chunk) || spillBackpressured)
691
+ return;
692
+ spillBackpressured = true;
693
+ pauseOutput();
694
+ spillStream.once("drain", () => {
695
+ spillBackpressured = false;
696
+ resumeOutput();
697
+ });
698
+ };
699
+ const startSpill = (chunk) => {
700
+ if (spillStream !== undefined) {
701
+ writeSpill(chunk);
702
+ return;
703
+ }
704
+ spillQueue.push(chunk);
705
+ if (spillStart !== undefined)
706
+ return;
707
+ pauseOutput();
708
+ spillStart = (async () => {
709
+ const created = await this.createTempFile({ prefix: "nyan-output-", suffix: ".log" }, context);
710
+ if (!created.ok)
711
+ throw created.error;
712
+ spillPath = created.value;
713
+ spillStream = createWriteStream(spillPath, { flags: "a", highWaterMark: SPILL_HIGH_WATER_MARK });
714
+ spillStream.on("error", failSpill);
715
+ for (const queued of spillQueue)
716
+ writeSpill(queued);
717
+ spillQueue.length = 0;
718
+ })()
719
+ .catch(failSpill)
720
+ .finally(resumeOutput);
721
+ };
722
+ const finishSpill = async () => {
723
+ await spillStart;
724
+ const stream = spillStream;
725
+ if (stream === undefined || spillError !== undefined || stream.destroyed)
726
+ return;
727
+ await new Promise((resolveFinish) => {
728
+ stream.once("error", () => resolveFinish());
729
+ stream.once("finish", resolveFinish);
730
+ stream.end();
731
+ });
732
+ };
733
+ try {
734
+ child = spawn(program, args, {
735
+ cwd,
736
+ detached: process.platform !== "win32",
737
+ env: getShellEnv(this.shellEnv, options?.env, options?.inheritEnv),
738
+ stdio: [stdinCommand === undefined ? "ignore" : "pipe", "pipe", "pipe"],
739
+ windowsHide: true,
740
+ });
741
+ if (child.pid)
742
+ this.activeChildPids.add(child.pid);
743
+ if (stdinCommand !== undefined) {
744
+ child.stdin?.on("error", () => { });
745
+ child.stdin?.end(stdinCommand);
746
+ }
747
+ }
748
+ catch (error) {
749
+ const cause = toError(error);
750
+ settle(err(new ExecutionError("spawn_error", cause.message, cause)));
751
+ return;
752
+ }
753
+ timeoutId =
754
+ timeoutMs === undefined
755
+ ? undefined
756
+ : setTimeout(() => {
757
+ timedOut = true;
758
+ onAbort();
759
+ }, timeoutMs);
760
+ if (signal) {
761
+ if (signal.aborted)
762
+ onAbort();
763
+ else
764
+ signal.addEventListener("abort", onAbort, { once: true });
765
+ }
766
+ const feed = (source, decoder) => (chunk) => {
767
+ emit(decoder.decode(chunk), source);
768
+ const spill = options?.spill;
769
+ if (spill === undefined || chunk.length === 0)
770
+ return;
771
+ if (spillStart !== undefined) {
772
+ startSpill(chunk);
773
+ return;
774
+ }
775
+ seenBytes += chunk.length;
776
+ for (let index = chunk.indexOf(0x0a); index !== -1; index = chunk.indexOf(0x0a, index + 1))
777
+ seenNewlines++;
778
+ const lines = seenNewlines + (chunk[chunk.length - 1] === 0x0a ? 0 : 1);
779
+ if (seenBytes <= spill.afterBytes && lines <= spill.afterLines) {
780
+ spillPrefix.push(chunk);
781
+ return;
782
+ }
783
+ for (const prefix of spillPrefix)
784
+ startSpill(prefix);
785
+ spillPrefix.length = 0;
786
+ startSpill(chunk);
787
+ };
788
+ child.stdout?.on("data", feed("stdout", stdoutDecoder));
789
+ child.stderr?.on("data", feed("stderr", stderrDecoder));
790
+ void waitForChildProcess(child, () => spillError === undefined &&
791
+ spillStart !== undefined &&
792
+ (spillStream === undefined || spillBackpressured)).then(async ({ code, signal: exitSignal }) => {
793
+ await finishSpill();
794
+ emit(stdoutDecoder.decode(), "stdout");
795
+ emit(stderrDecoder.decode(), "stderr");
796
+ if (callbackError) {
797
+ settle(err(callbackError));
798
+ return;
799
+ }
800
+ const interrupted = timedOut
801
+ ? new ExecutionError("timeout", `timeout:${options?.timeout}`)
802
+ : signal?.aborted
803
+ ? new ExecutionError("aborted", "aborted")
804
+ : undefined;
805
+ if (interrupted !== undefined) {
806
+ if (spillPath !== undefined)
807
+ interrupted.spillPath = spillPath;
808
+ settle(err(interrupted));
809
+ return;
810
+ }
811
+ if (spillError) {
812
+ settle(err(spillError));
813
+ return;
814
+ }
815
+ // A process killed by a signal (e.g. OOM killer) has no exit code; map it
816
+ // to the conventional 128 + signal number so callers do not mistake it
817
+ // for a successful exit.
818
+ const exitCode = code ?? (exitSignal ? 128 + (osConstants.signals[exitSignal] ?? 0) : 1);
819
+ settle(ok({ exitCode, ...(spillPath === undefined ? {} : { spillPath }) }));
820
+ }, (error) => settle(err(new ExecutionError("spawn_error", error.message, error))));
821
+ });
822
+ }
823
+ async openTextLineReader(path, context) {
824
+ const resolved = resolvePath(this.cwd, path);
825
+ const aborted = abortResult(context.abortSignal, resolved);
826
+ if (aborted)
827
+ return aborted;
828
+ try {
829
+ const file = await openFile(resolved, "r");
830
+ const afterOpenAbort = abortResult(context.abortSignal, resolved);
831
+ if (afterOpenAbort) {
832
+ await file.close().catch(() => undefined);
833
+ return afterOpenAbort;
834
+ }
835
+ return ok(new NodeTextLineReader(file, resolved));
836
+ }
837
+ catch (error) {
838
+ return err(toFileError(error, resolved));
839
+ }
840
+ }
841
+ async readTextFile(path, context) {
842
+ const resolved = resolvePath(this.cwd, path);
843
+ const signal = context.abortSignal;
844
+ const aborted = abortResult(signal, resolved);
845
+ if (aborted)
846
+ return aborted;
847
+ try {
848
+ return ok(await readFile(resolved, { encoding: "utf8", signal }));
849
+ }
850
+ catch (error) {
851
+ return err(toFileError(error, resolved));
852
+ }
853
+ }
854
+ async readTextLines(path, options, context) {
855
+ if (options?.maxLines !== undefined && options.maxLines <= 0)
856
+ return ok([]);
857
+ const opened = await this.openTextLineReader(path, context);
858
+ if (!opened.ok)
859
+ return opened;
860
+ const lines = [];
861
+ try {
862
+ while (options?.maxLines === undefined || lines.length < options.maxLines) {
863
+ const line = await opened.value.readLine(context);
864
+ if (!line.ok)
865
+ return line;
866
+ if (line.value === undefined)
867
+ break;
868
+ lines.push(line.value.text);
869
+ }
870
+ return ok(lines);
871
+ }
872
+ finally {
873
+ await opened.value.close(context);
874
+ }
875
+ }
876
+ async readBinaryFile(path, context) {
877
+ const resolved = resolvePath(this.cwd, path);
878
+ const signal = context.abortSignal;
879
+ const aborted = abortResult(signal, resolved);
880
+ if (aborted)
881
+ return aborted;
882
+ try {
883
+ return ok(await readFile(resolved, { signal }));
884
+ }
885
+ catch (error) {
886
+ return err(toFileError(error, resolved));
887
+ }
888
+ }
889
+ async openBinaryReader(path, options, context) {
890
+ const resolved = resolvePath(this.cwd, path);
891
+ const aborted = abortResult(context.abortSignal, resolved);
892
+ if (aborted)
893
+ return aborted;
894
+ const noFollow = options?.noFollow === true;
895
+ let file;
896
+ try {
897
+ let flags = constants.O_RDONLY;
898
+ if (process.platform === "win32") {
899
+ // Windows has no O_NOFOLLOW; check the final component before opening (not race-free).
900
+ if (noFollow && (await lstat(resolved)).isSymbolicLink())
901
+ return err(symlinkRefused(resolved));
902
+ }
903
+ else {
904
+ // Nonblocking, so opening a FIFO does not wait for a writer.
905
+ flags |= constants.O_NONBLOCK;
906
+ if (noFollow)
907
+ flags |= constants.O_NOFOLLOW;
908
+ }
909
+ file = await openFile(resolved, flags);
910
+ const stats = await file.stat();
911
+ if (!stats.isFile()) {
912
+ await file.close().catch(() => undefined);
913
+ return err(stats.isDirectory()
914
+ ? new FileError("is_directory", "EISDIR: illegal operation on a directory, read", resolved)
915
+ : new FileError("invalid", "Not a regular file", resolved));
916
+ }
917
+ const afterOpenAbort = abortResult(context.abortSignal, resolved);
918
+ if (afterOpenAbort) {
919
+ await file.close().catch(() => undefined);
920
+ return afterOpenAbort;
921
+ }
922
+ return ok(new NodeBinaryReader(file, resolved));
923
+ }
924
+ catch (error) {
925
+ await file?.close().catch(() => undefined);
926
+ // O_NOFOLLOW reports a final-component symlink as ELOOP (EMLINK on some BSDs).
927
+ if (noFollow && isNodeError(error) && (error.code === "ELOOP" || error.code === "EMLINK")) {
928
+ return err(symlinkRefused(resolved, toError(error)));
929
+ }
930
+ return err(toFileError(error, resolved));
931
+ }
932
+ }
933
+ async writeFile(path, content, context) {
934
+ const resolved = resolvePath(this.cwd, path);
935
+ const signal = context.abortSignal;
936
+ const aborted = abortResult(signal, resolved);
937
+ if (aborted)
938
+ return aborted;
939
+ try {
940
+ await mkdir(resolve(resolved, ".."), { recursive: true });
941
+ const afterMkdirAbort = abortResult(signal, resolved);
942
+ if (afterMkdirAbort)
943
+ return afterMkdirAbort;
944
+ await writeFile(resolved, content, { signal });
945
+ return ok(undefined);
946
+ }
947
+ catch (error) {
948
+ return err(toFileError(error, resolved));
949
+ }
950
+ }
951
+ async appendFile(path, content, context) {
952
+ const resolved = resolvePath(this.cwd, path);
953
+ const signal = context.abortSignal;
954
+ const aborted = abortResult(signal, resolved);
955
+ if (aborted)
956
+ return aborted;
957
+ try {
958
+ await mkdir(resolve(resolved, ".."), { recursive: true });
959
+ const afterMkdirAbort = abortResult(signal, resolved);
960
+ if (afterMkdirAbort)
961
+ return afterMkdirAbort;
962
+ await appendFile(resolved, content);
963
+ const afterAppendAbort = abortResult(signal, resolved);
964
+ return afterAppendAbort ?? ok(undefined);
965
+ }
966
+ catch (error) {
967
+ return err(toFileError(error, resolved));
968
+ }
969
+ }
970
+ async truncateFile(path, size, context) {
971
+ const resolved = resolvePath(this.cwd, path);
972
+ const signal = context.abortSignal;
973
+ const aborted = abortResult(signal, resolved);
974
+ if (aborted)
975
+ return aborted;
976
+ if (!Number.isSafeInteger(size) || size < 0) {
977
+ return err(new FileError("invalid", "File size must be a non-negative safe integer", resolved));
978
+ }
979
+ let file;
980
+ try {
981
+ file = await openFile(resolved, "r+");
982
+ await file.truncate(size);
983
+ const afterTruncateAbort = abortResult(signal, resolved);
984
+ return afterTruncateAbort ?? ok(undefined);
985
+ }
986
+ catch (error) {
987
+ return err(toFileError(error, resolved));
988
+ }
989
+ finally {
990
+ await file?.close().catch(() => undefined);
991
+ }
992
+ }
993
+ async flushFile(path, context) {
994
+ const resolved = resolvePath(this.cwd, path);
995
+ const signal = context.abortSignal;
996
+ const aborted = abortResult(signal, resolved);
997
+ if (aborted)
998
+ return aborted;
999
+ let file;
1000
+ try {
1001
+ file = await openFile(resolved, "r+");
1002
+ // POSIX refuses to open a directory for writing; Windows opens it, so check explicitly.
1003
+ if ((await file.stat()).isDirectory())
1004
+ return err(new FileError("is_directory", "Is a directory", resolved));
1005
+ await file.sync();
1006
+ const afterSyncAbort = abortResult(signal, resolved);
1007
+ return afterSyncAbort ?? ok(undefined);
1008
+ }
1009
+ catch (error) {
1010
+ return err(toFileError(error, resolved));
1011
+ }
1012
+ finally {
1013
+ await file?.close().catch(() => undefined);
1014
+ }
1015
+ }
1016
+ async renameFile(sourcePath, destinationPath, context) {
1017
+ const source = resolvePath(this.cwd, sourcePath);
1018
+ const destination = resolvePath(this.cwd, destinationPath);
1019
+ const aborted = abortResult(context.abortSignal, destination);
1020
+ if (aborted)
1021
+ return aborted;
1022
+ try {
1023
+ await rename(source, destination);
1024
+ return ok(undefined);
1025
+ }
1026
+ catch (error) {
1027
+ return err(toFileError(error, source));
1028
+ }
1029
+ }
1030
+ async fileInfo(path, context) {
1031
+ const resolved = resolvePath(this.cwd, path);
1032
+ const aborted = abortResult(context.abortSignal, resolved);
1033
+ if (aborted)
1034
+ return aborted;
1035
+ try {
1036
+ return fileInfoFromStats(resolved, await lstat(resolved));
1037
+ }
1038
+ catch (error) {
1039
+ return err(toFileError(error, resolved));
1040
+ }
1041
+ }
1042
+ async listDir(path, context) {
1043
+ const resolved = resolvePath(this.cwd, path);
1044
+ const signal = context.abortSignal;
1045
+ const aborted = abortResult(signal, resolved);
1046
+ if (aborted)
1047
+ return aborted;
1048
+ try {
1049
+ const entries = await readdir(resolved, { withFileTypes: true });
1050
+ const infos = [];
1051
+ for (const entry of entries) {
1052
+ const loopAbort = abortResult(signal, resolved);
1053
+ if (loopAbort)
1054
+ return loopAbort;
1055
+ const entryPath = resolve(resolved, entry.name);
1056
+ try {
1057
+ const info = fileInfoFromStats(entryPath, await lstat(entryPath));
1058
+ if (info.ok)
1059
+ infos.push(info.value);
1060
+ }
1061
+ catch (error) {
1062
+ return err(toFileError(error, entryPath));
1063
+ }
1064
+ }
1065
+ return ok(infos);
1066
+ }
1067
+ catch (error) {
1068
+ return err(toFileError(error, resolved));
1069
+ }
1070
+ }
1071
+ async openDirReader(path, context) {
1072
+ const resolved = resolvePath(this.cwd, path);
1073
+ const aborted = abortResult(context.abortSignal, resolved);
1074
+ if (aborted)
1075
+ return aborted;
1076
+ try {
1077
+ const dir = await opendir(resolved);
1078
+ const afterOpenAbort = abortResult(context.abortSignal, resolved);
1079
+ if (afterOpenAbort) {
1080
+ await dir.close().catch(() => undefined);
1081
+ return afterOpenAbort;
1082
+ }
1083
+ return ok(new NodeDirReader(dir, resolved));
1084
+ }
1085
+ catch (error) {
1086
+ return err(toFileError(error, resolved));
1087
+ }
1088
+ }
1089
+ async canonicalPath(path, context) {
1090
+ const resolved = resolvePath(this.cwd, path);
1091
+ const aborted = abortResult(context.abortSignal, resolved);
1092
+ if (aborted)
1093
+ return aborted;
1094
+ try {
1095
+ return ok(await realpath(resolved));
1096
+ }
1097
+ catch (error) {
1098
+ return err(toFileError(error, resolved));
1099
+ }
1100
+ }
1101
+ async exists(path, context) {
1102
+ const result = await this.fileInfo(path, context);
1103
+ if (result.ok)
1104
+ return ok(true);
1105
+ if (result.error.code === "not_found")
1106
+ return ok(false);
1107
+ return err(result.error);
1108
+ }
1109
+ async createDir(path, options, context) {
1110
+ const resolved = resolvePath(this.cwd, path);
1111
+ const aborted = abortResult(context.abortSignal, resolved);
1112
+ if (aborted)
1113
+ return aborted;
1114
+ try {
1115
+ await mkdir(resolved, { recursive: options?.recursive ?? true });
1116
+ return ok(undefined);
1117
+ }
1118
+ catch (error) {
1119
+ return err(toFileError(error, resolved));
1120
+ }
1121
+ }
1122
+ async remove(path, options, context) {
1123
+ const resolved = resolvePath(this.cwd, path);
1124
+ const aborted = abortResult(context.abortSignal, resolved);
1125
+ if (aborted)
1126
+ return aborted;
1127
+ try {
1128
+ await rm(resolved, { recursive: options?.recursive ?? false, force: options?.force ?? false });
1129
+ return ok(undefined);
1130
+ }
1131
+ catch (error) {
1132
+ return err(toFileError(error, resolved));
1133
+ }
1134
+ }
1135
+ async createTempDir(prefix, context) {
1136
+ const aborted = abortResult(context.abortSignal);
1137
+ if (aborted)
1138
+ return aborted;
1139
+ try {
1140
+ prefix ??= "tmp-";
1141
+ return ok(await mkdtemp(join(tmpdir(), prefix)));
1142
+ }
1143
+ catch (error) {
1144
+ return err(toFileError(error));
1145
+ }
1146
+ }
1147
+ async createTempFile(options, context) {
1148
+ const dir = await this.createTempDir("tmp-", context);
1149
+ if (!dir.ok)
1150
+ return dir;
1151
+ const filePath = join(dir.value, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`);
1152
+ try {
1153
+ await writeFile(filePath, "");
1154
+ return ok(filePath);
1155
+ }
1156
+ catch (error) {
1157
+ return err(toFileError(error, filePath));
1158
+ }
1159
+ }
1160
+ async cleanup(_context) {
1161
+ for (const pid of this.activeChildPids)
1162
+ killProcessTree(pid);
1163
+ this.activeChildPids.clear();
1164
+ }
1165
+ }
1166
+ //# sourceMappingURL=node.js.map