//#region src/contexts/docker.ts
const SINGLE_QUOTE_RE = /'/g;
function createDockerContext(config) {
	let counter = 0;
	const containers = /* @__PURE__ */ new Map();
	const defaultImage = config?.image ?? "oven/bun:latest";
	const defaultCwd = config?.cwd ?? "/workspace";
	const defaultEnv = config?.env;
	const defaultLimits = config?.limits;
	const defaultMounts = config?.mounts ?? [];
	const defaultName = config?.name;
	async function getDockerode() {
		try {
			const Dockerode = (await import("dockerode")).default;
			return new Dockerode();
		} catch {
			throw new Error("dockerode is required for Docker execution context. Install it with: bun add dockerode");
		}
	}
	const ctx = {
		type: "docker",
		capabilities: {
			shell: true,
			filesystem: true,
			network: true,
			gpu: false
		},
		async spawn(overrides) {
			const docker = await getDockerode();
			const id = `docker-${++counter}`;
			const image = overrides?.image ?? defaultImage;
			const cwd = overrides?.cwd ?? defaultCwd;
			const mounts = [...defaultMounts, ...overrides?.mounts ?? []];
			const name = overrides?.name ?? defaultName;
			try {
				await docker.getImage(image).inspect();
			} catch {
				await new Promise((resolve, reject) => {
					docker.pull(image, (err, stream) => {
						if (err) return reject(err);
						docker.modem.followProgress(stream, (err2) => {
							err2 ? reject(err2) : resolve();
						});
					});
				});
			}
			const limits = {
				...defaultLimits,
				...overrides?.limits
			};
			const hostConfig = {};
			if (limits?.memory) hostConfig.Memory = limits.memory * 1024 * 1024;
			if (limits?.cpu) hostConfig.NanoCpus = Number.parseFloat(limits.cpu) * 1e9;
			const env = {
				...defaultEnv,
				...overrides?.env
			};
			const bindSpecs = buildBindSpecs(mounts);
			if (bindSpecs.length > 0) hostConfig.Binds = bindSpecs;
			const portSpec = overrides?.ports ?? config?.ports;
			const exposedPorts = {};
			const portBindings = {};
			if (portSpec?.length) {
				for (const p of portSpec) {
					const key = `${p.container}/${p.proto ?? "tcp"}`;
					exposedPorts[key] = {};
					portBindings[key] = [{ HostPort: p.host == null ? "" : String(p.host) }];
				}
				hostConfig.PortBindings = portBindings;
			}
			const user = overrides?.user ?? config?.user;
			const network = overrides?.network ?? config?.network;
			if (network) hostConfig.NetworkMode = network;
			const labels = {
				...config?.labels,
				...overrides?.labels
			};
			const container = await docker.createContainer({
				...name ? { name: `${name}-${counter}-${Math.random().toString(36).slice(2, 8)}` } : {},
				Image: image,
				Cmd: ["sleep", "infinity"],
				WorkingDir: cwd,
				Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),
				HostConfig: hostConfig,
				...Object.keys(exposedPorts).length ? { ExposedPorts: exposedPorts } : {},
				...user ? { User: user } : {},
				...Object.keys(labels).length ? { Labels: labels } : {}
			});
			await container.start();
			const handle = {
				id,
				type: "docker",
				cwd
			};
			containers.set(id, {
				handle,
				container,
				docker
			});
			return handle;
		},
		async exec(handle, command, options) {
			const ref = containers.get(handle.id);
			if (!ref) throw new Error(`Container ${handle.id} not found`);
			const execCwd = options?.cwd ?? handle.cwd;
			const env = options?.env ? Object.entries(options.env).map(([k, v]) => `${k}=${v}`) : [];
			const exec = await ref.container.exec({
				Cmd: [
					"sh",
					"-c",
					command
				],
				WorkingDir: execCwd,
				Env: env,
				AttachStdout: true,
				AttachStderr: true
			});
			const stream = await exec.start({ Detach: false });
			return new Promise((resolve) => {
				let stdout = "";
				let stderr = "";
				let resolved = false;
				const stdoutSink = {
					write(chunk) {
						stdout += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
						return true;
					},
					end() {},
					on() {},
					once() {},
					emit() {
						return true;
					}
				};
				const stderrSink = {
					write(chunk) {
						stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
						return true;
					},
					end() {},
					on() {},
					once() {},
					emit() {
						return true;
					}
				};
				try {
					ref.docker.modem.demuxStream(stream, stdoutSink, stderrSink);
				} catch {
					stream.on("data", (chunk) => {
						stdout += chunk.toString("utf-8");
					});
				}
				const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30;
				const timer = setTimeout(() => {
					if (resolved) return;
					resolved = true;
					try {
						stream.destroy?.();
					} catch {}
					resolve({
						stdout,
						stderr: stderr ? `${stderr}\n[timeout]` : "[timeout]",
						exitCode: 124
					});
				}, timeout * 1e3);
				stream.on("end", async () => {
					if (resolved) return;
					resolved = true;
					clearTimeout(timer);
					let exitCode = 0;
					try {
						exitCode = (await exec.inspect()).ExitCode ?? 0;
					} catch {}
					resolve({
						stdout,
						stderr,
						exitCode
					});
				});
				stream.on("error", (err) => {
					if (resolved) return;
					resolved = true;
					clearTimeout(timer);
					resolve({
						stdout,
						stderr: stderr ? `${stderr}\n${err.message}` : err.message,
						exitCode: 1
					});
				});
			});
		},
		async readFile(handle, path) {
			const result = await ctx.exec(handle, `cat ${JSON.stringify(path)}`);
			if (result.exitCode !== 0) throw new Error(`Failed to read file: ${result.stderr}`);
			return result.stdout;
		},
		async writeFile(handle, path, content) {
			const escaped = content.replace(SINGLE_QUOTE_RE, String.raw`'\''`);
			const result = await ctx.exec(handle, `mkdir -p "$(dirname ${JSON.stringify(path)})" && printf '%s' '${escaped}' > ${JSON.stringify(path)}`);
			if (result.exitCode !== 0) throw new Error(`Failed to write file: ${result.stderr}`);
		},
		async listFiles(handle, path) {
			const result = await ctx.exec(handle, `ls -1 ${JSON.stringify(path)}`);
			if (result.exitCode !== 0) return [];
			return result.stdout.trim().split("\n").filter(Boolean);
		},
		async getMappedPort(handle, containerPort) {
			const ref = containers.get(handle.id);
			if (!ref) return null;
			const bindings = (await ref.container.inspect())?.NetworkSettings?.Ports ?? {};
			for (const proto of ["tcp", "udp"]) {
				const list = bindings[`${containerPort}/${proto}`];
				if (Array.isArray(list) && list.length > 0 && list[0]?.HostPort) return Number.parseInt(list[0].HostPort, 10);
			}
			return null;
		},
		async destroy(handle) {
			const ref = containers.get(handle.id);
			if (!ref) return;
			try {
				await ref.container.stop({ t: 5 });
				await ref.container.remove({ force: true });
			} catch {}
			containers.delete(handle.id);
		}
	};
	return ctx;
}
/**
* Format one {@link ContextMount} as a dockerode `HostConfig.Binds` entry.
* Supports read-only (`:ro`) and SELinux shared-label (`:z`) options; the two
* are mutually exclusive (a read-only mount can't also be a shared rw mount).
*/
function formatBindMount(mount) {
	if (!mount.source) throw new Error("Docker mount source is required");
	if (!mount.target || !mount.target.startsWith("/")) throw new Error(`Docker mount target must be an absolute path: ${mount.target}`);
	if (mount.readonly && mount.shared) throw new Error(`Docker mount cannot be both readonly and shared: ${mount.target}`);
	const opts = mount.readonly ? ":ro" : mount.shared ? ":z" : "";
	return `${mount.source}:${mount.target}${opts}`;
}
/** Assemble dockerode `HostConfig.Binds` from the context's `mounts`. */
function buildBindSpecs(mounts) {
	return mounts.map(formatBindMount);
}
//#endregion
export { buildBindSpecs, createDockerContext };

//# sourceMappingURL=docker.js.map