@snappedly-tools/shipyard 0.7.0-staging.36082887921 → 0.7.0

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 (41) hide show
  1. package/README.md +41 -34
  2. package/dist/MountConfig-bZoCs4Dd.d.ts +26 -0
  3. package/dist/SandboxProvider-XJQqEdSf.d.ts +261 -0
  4. package/dist/{chunk-JI3HDDMS.js → chunk-56TDSWFU.js} +3 -3
  5. package/dist/{chunk-JI3HDDMS.js.map → chunk-56TDSWFU.js.map} +1 -1
  6. package/dist/chunk-ACD46ZM4.js +136 -0
  7. package/dist/chunk-ACD46ZM4.js.map +1 -0
  8. package/dist/{chunk-L6PX5QTU.js → chunk-FDYOTN55.js} +887 -262
  9. package/dist/chunk-FDYOTN55.js.map +1 -0
  10. package/dist/chunk-KMGNFXKN.js +38 -0
  11. package/dist/chunk-KMGNFXKN.js.map +1 -0
  12. package/dist/chunk-SOJTAJTF.js +78 -0
  13. package/dist/chunk-SOJTAJTF.js.map +1 -0
  14. package/dist/{chunk-44I2BL6E.js → chunk-WAXEMZUV.js} +33 -225
  15. package/dist/chunk-WAXEMZUV.js.map +1 -0
  16. package/dist/index.d.ts +97 -117
  17. package/dist/index.js +344 -322
  18. package/dist/index.js.map +1 -1
  19. package/dist/main.js +139 -235
  20. package/dist/main.js.map +1 -1
  21. package/dist/sandboxes/docker.d.ts +3 -1
  22. package/dist/sandboxes/docker.js +4 -2
  23. package/dist/sandboxes/no-sandbox.d.ts +37 -0
  24. package/dist/sandboxes/no-sandbox.js +4 -0
  25. package/dist/sandboxes/no-sandbox.js.map +1 -0
  26. package/dist/sandboxes/vercel.d.ts +104 -0
  27. package/dist/sandboxes/vercel.js +166 -0
  28. package/dist/sandboxes/vercel.js.map +1 -0
  29. package/dist/templates/blank/main.mts +13 -0
  30. package/dist/templates/blank/prompt.md +12 -0
  31. package/dist/templates/blank/template.json +4 -0
  32. package/dist/templates/parallel-planner/main.mts +9 -67
  33. package/dist/templates/parallel-planner-with-review/main.mts +12 -70
  34. package/dist/templates/sequential-reviewer/main.mts +3 -52
  35. package/dist/templates/simple-loop/main.mts +2 -51
  36. package/package.json +17 -1
  37. package/dist/MountConfig-BHnKnA4h.d.ts +0 -145
  38. package/dist/chunk-44I2BL6E.js.map +0 -1
  39. package/dist/chunk-L6PX5QTU.js.map +0 -1
  40. package/dist/templates/parallel-planner/planner-branch.mts +0 -151
  41. package/dist/templates/parallel-planner-with-review/planner-branch.mts +0 -151
@@ -1,15 +1,15 @@
1
- import { existsSync, constants } from 'fs';
2
- import { writeFile, chmod, mkdir, stat, copyFile, rm, readdir, mkdtemp, lstat, readFile, open } from 'fs/promises';
1
+ import { toSessionTransferHandle, shellQuote } from './chunk-KMGNFXKN.js';
2
+ import { existsSync, constants, lstatSync, realpathSync, statSync } from 'fs';
3
+ import { mkdtemp, writeFile, chmod, mkdir, stat, copyFile, rm, readdir, lstat, readFile, open } from 'fs/promises';
3
4
  import * as clack from '@clack/prompts';
4
5
  import { FileSystem } from '@effect/platform';
5
- import { posix, join, basename, dirname, resolve, relative, sep, isAbsolute, normalize } from 'path';
6
+ import { join, basename, posix, dirname, resolve, normalize, relative, sep, isAbsolute } from 'path';
6
7
  import { Context, Layer, Effect, Data, Exit, Duration, Option, Ref, Deferred, Schedule } from 'effect';
7
8
  import { styleText, promisify } from 'util';
9
+ import { tmpdir, homedir } from 'os';
8
10
  import { randomBytes } from 'crypto';
9
11
  import { exec, execFile } from 'child_process';
10
- import { tmpdir } from 'os';
11
12
 
12
- // src/Display.ts
13
13
  var Display = class extends Context.Tag("Display")() {
14
14
  };
15
15
  var SilentDisplay = {
@@ -259,67 +259,6 @@ var ClackDisplay = {
259
259
  )
260
260
  })
261
261
  };
262
-
263
- // src/SandboxProvider.ts
264
- var createIsolatedSandboxProvider = (config) => ({
265
- tag: "isolated",
266
- name: config.name,
267
- env: config.env ?? {},
268
- create: config.create
269
- });
270
- var toSessionTransferHandle = (handle) => handle === void 0 ? void 0 : "copyFileIn" in handle ? handle : {
271
- worktreePath: handle.worktreePath,
272
- exec: (command, options) => handle.exec(command, options),
273
- interactiveExec: handle.interactiveExec?.bind(handle),
274
- copyFileIn: (source, destination) => handle.copyIn(source, destination),
275
- copyFileOut: (source, destination) => handle.copyFileOut(source, destination),
276
- close: () => handle.close()
277
- };
278
-
279
- // src/shutdownRegistry.ts
280
- var teardownCallbacks = /* @__PURE__ */ new Set();
281
- var listenersInstalled = false;
282
- var runTeardowns = () => {
283
- for (const teardown of teardownCallbacks) {
284
- try {
285
- teardown();
286
- } catch {
287
- }
288
- }
289
- };
290
- var handleExit = () => {
291
- runTeardowns();
292
- };
293
- var handleSignal = () => {
294
- detachListeners();
295
- runTeardowns();
296
- process.exit(1);
297
- };
298
- var attachListeners = () => {
299
- if (listenersInstalled) return;
300
- listenersInstalled = true;
301
- process.on("exit", handleExit);
302
- process.on("SIGINT", handleSignal);
303
- process.on("SIGTERM", handleSignal);
304
- };
305
- var detachListeners = () => {
306
- if (!listenersInstalled) return;
307
- listenersInstalled = false;
308
- process.removeListener("exit", handleExit);
309
- process.removeListener("SIGINT", handleSignal);
310
- process.removeListener("SIGTERM", handleSignal);
311
- };
312
- var registerShutdown = (teardown) => {
313
- teardownCallbacks.add(teardown);
314
- attachListeners();
315
- let active = true;
316
- return () => {
317
- if (!active) return;
318
- active = false;
319
- teardownCallbacks.delete(teardown);
320
- if (teardownCallbacks.size === 0) detachListeners();
321
- };
322
- };
323
262
  var CwdError = class extends Data.TaggedError("CwdError") {
324
263
  };
325
264
  var resolveCwd = (cwd) => Effect.gen(function* () {
@@ -341,6 +280,8 @@ var resolveCwd = (cwd) => Effect.gen(function* () {
341
280
  }
342
281
  return resolved;
343
282
  });
283
+
284
+ // src/errors.ts
344
285
  var ExecError = class extends Data.TaggedError("ExecError") {
345
286
  };
346
287
  var ExecHostError = class extends Data.TaggedError("ExecHostError") {
@@ -423,7 +364,10 @@ var WORKTREES_DIR = "worktrees";
423
364
  var PATCHES_DIR = "patches";
424
365
  var LOCKS_DIR = "locks";
425
366
  var RUNNER_DIR = "runner";
367
+ var RUNNER_SANDBOX_MASK_DIR = "runner-sandbox-mask";
368
+ var PARENT_GIT_SANDBOX_DIR = "/.shipyard-parent-git";
426
369
  var SYNC_BASE_REF = "refs/shipyard/sync-base";
370
+ var CONTAINER_NAME_PREFIX = "shipyard-";
427
371
  var WINDOWS_DRIVE_PATH = /^[A-Za-z]:/;
428
372
  var assertSafeRelativePath = (relativePath, label = "relative path") => {
429
373
  if (relativePath.length === 0 || relativePath.includes("\0") || isAbsolute(relativePath) || WINDOWS_DRIVE_PATH.test(relativePath) || relativePath.startsWith("\\\\") || relativePath.split(/[\\/]/).some((part) => part === "..")) {
@@ -496,7 +440,7 @@ var SAFE_CONFIG = [
496
440
  ["checkout.recurseSubmodules", "false"]
497
441
  ];
498
442
  var COMMAND_CONFIG_PATTERN = "^(filter\\..*\\.(clean|smudge|process|required)|merge\\..*\\.driver|diff\\..*\\.(command|textconv)|difftool\\..*\\.cmd|mergetool\\..*\\.cmd)$";
499
- var runGit = (args, cwd, options = {}) => new Promise((resolve4, reject) => {
443
+ var runGit = (args, cwd, options = {}) => new Promise((resolve6, reject) => {
500
444
  execFile(
501
445
  "git",
502
446
  [...args],
@@ -514,11 +458,11 @@ var runGit = (args, cwd, options = {}) => new Promise((resolve4, reject) => {
514
458
  },
515
459
  (error, stdout, stderr) => {
516
460
  if (!error) {
517
- resolve4({ stdout, stderr, exitCode: 0 });
461
+ resolve6({ stdout, stderr, exitCode: 0 });
518
462
  return;
519
463
  }
520
464
  if (typeof error.code === "number") {
521
- resolve4({ stdout, stderr, exitCode: error.code });
465
+ resolve6({ stdout, stderr, exitCode: error.code });
522
466
  return;
523
467
  }
524
468
  reject(error);
@@ -925,14 +869,6 @@ var pruneStale = (repoDir) => Effect.gen(function* () {
925
869
  })
926
870
  )
927
871
  );
928
-
929
- // src/shellQuote.ts
930
- var shellQuote = (value) => {
931
- if (value.includes("\0")) {
932
- throw new Error("Cannot quote a string containing a NUL byte");
933
- }
934
- return `'${value.replaceAll("'", "'\\''")}'`;
935
- };
936
872
  var PROTECTED_RUNNER_PATH = `${CONFIG_DIR}/${RUNNER_DIR}`;
937
873
  var pathKey = (value) => value.normalize("NFC").toLocaleLowerCase("en-US");
938
874
  var SAFE_RUNNER_ENV_KEYS = /* @__PURE__ */ new Set([
@@ -964,14 +900,121 @@ var assertExcludesRepositoryRunner = (relativePath) => {
964
900
  );
965
901
  }
966
902
  };
967
- var assertProtectedDirectoryIdentities = async (runnerDir, inspect) => {
903
+ var assertSafeRepositoryRunnerDirectories = (repoDir) => {
904
+ const runnerDir = join(repoDir, CONFIG_DIR, RUNNER_DIR);
905
+ const maskDir = join(repoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR);
906
+ for (const [name, path] of [
907
+ ["repository runner", runnerDir],
908
+ ["repository runner sandbox mask", maskDir]
909
+ ]) {
910
+ const stat2 = lstatSync(path);
911
+ if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
912
+ throw new Error(
913
+ `The protected ${name} path must be a real directory: ${path}`
914
+ );
915
+ }
916
+ }
917
+ if (pathKey(realpathSync(runnerDir)) === pathKey(realpathSync(maskDir))) {
918
+ throw new Error(
919
+ "The protected repository runner and sandbox mask paths must be distinct directories."
920
+ );
921
+ }
922
+ };
923
+ var assertProtectedDirectoryIdentities = async (runnerDir, maskDir, inspect) => {
968
924
  if (!inspect) return;
969
925
  const runner = await inspect(runnerDir);
926
+ const mask = await inspect(maskDir);
970
927
  if (!runner.directory || runner.symbolicLink) {
971
928
  throw new Error(
972
929
  `The protected repository runner path must be a real directory: ${runnerDir}`
973
930
  );
974
931
  }
932
+ if (!mask.directory || mask.symbolicLink) {
933
+ throw new Error(
934
+ `The protected repository runner sandbox mask path must be a real directory: ${maskDir}`
935
+ );
936
+ }
937
+ if (pathKey(runner.realPath) === pathKey(mask.realPath)) {
938
+ throw new Error(
939
+ "The protected repository runner and sandbox mask paths must be distinct directories."
940
+ );
941
+ }
942
+ };
943
+
944
+ // src/CopyToWorktree.ts
945
+ var COPY_TO_WORKTREE_TIMEOUT_MS = 6e4;
946
+ var getCopyOnWriteFlags = (platform) => platform === "darwin" ? ["-cR"] : ["-R", "--reflink=auto"];
947
+ var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
948
+ const effectiveTimeout = timeoutMs ?? COPY_TO_WORKTREE_TIMEOUT_MS;
949
+ return Effect.gen(function* () {
950
+ const cowFlags = getCopyOnWriteFlags(process.platform);
951
+ for (const relativePath of paths) {
952
+ let src;
953
+ let dest;
954
+ try {
955
+ assertExcludesRepositoryRunner(relativePath);
956
+ src = resolveSafeRelativePath(hostRepoDir, relativePath, "copy path");
957
+ dest = resolveSafeRelativePath(worktreePath, relativePath, "copy path");
958
+ } catch (error) {
959
+ return yield* Effect.fail(
960
+ new CopyToWorktreeError({
961
+ message: `Refusing unsafe copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
962
+ path: relativePath,
963
+ stderr: error instanceof Error ? error.message : String(error),
964
+ exitCode: null
965
+ })
966
+ );
967
+ }
968
+ if (!existsSync(src)) {
969
+ continue;
970
+ }
971
+ yield* Effect.tryPromise({
972
+ try: async () => {
973
+ await assertNoSymlinkComponents(hostRepoDir, src, "copy source");
974
+ await assertNoSymlinkComponents(worktreePath, dest, "copy target");
975
+ },
976
+ catch: (error) => new CopyToWorktreeError({
977
+ message: `Refusing symlinked copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
978
+ path: relativePath,
979
+ stderr: error instanceof Error ? error.message : String(error),
980
+ exitCode: null
981
+ })
982
+ });
983
+ yield* Effect.async((resume) => {
984
+ execFile("cp", [...cowFlags, src, dest], (error) => {
985
+ if (error) {
986
+ execFile("cp", ["-R", src, dest], (fallbackError, _, stderr) => {
987
+ if (fallbackError) {
988
+ resume(
989
+ Effect.fail(
990
+ new CopyToWorktreeError({
991
+ message: `Failed to copy ${relativePath} to worktree: ${stderr || fallbackError.message}`,
992
+ path: relativePath,
993
+ stderr: stderr || fallbackError.message,
994
+ exitCode: typeof fallbackError.code === "number" ? fallbackError.code : null
995
+ })
996
+ )
997
+ );
998
+ } else {
999
+ resume(Effect.succeed(void 0));
1000
+ }
1001
+ });
1002
+ } else {
1003
+ resume(Effect.succeed(void 0));
1004
+ }
1005
+ });
1006
+ });
1007
+ }
1008
+ }).pipe(
1009
+ withTimeout(
1010
+ effectiveTimeout,
1011
+ () => new CopyToWorktreeTimeoutError({
1012
+ message: `Copying files to worktree timed out after ${effectiveTimeout}ms`,
1013
+ timeoutMs: effectiveTimeout,
1014
+ paths
1015
+ })
1016
+ )
1017
+ );
975
1018
  };
976
1019
 
977
1020
  // src/RecoveryMessage.ts
@@ -1481,6 +1524,8 @@ ${msg}`);
1481
1524
  });
1482
1525
  }
1483
1526
  });
1527
+
1528
+ // src/SandboxLifecycle.ts
1484
1529
  var GIT_SETUP_TIMEOUT_MS = 1e4;
1485
1530
  var HOOK_TIMEOUT_MS = 6e4;
1486
1531
  var COMMIT_COLLECTION_TIMEOUT_MS = 3e4;
@@ -1814,169 +1859,6 @@ ${err instanceof Error ? err.message : String(err)}`
1814
1859
  }
1815
1860
  return { result, branch: finalBranch, commits };
1816
1861
  });
1817
- var makeSandboxFromHandle = (handle) => ({
1818
- exec: (command, options) => Effect.tryPromise({
1819
- try: () => handle.exec(command, options),
1820
- catch: (e) => new ExecError({
1821
- command,
1822
- message: `exec failed: ${e instanceof Error ? e.message : String(e)}`
1823
- })
1824
- }),
1825
- copyIn: (hostPath, sandboxPath) => Effect.tryPromise({
1826
- try: () => handle.copyIn(hostPath, sandboxPath),
1827
- catch: (e) => new CopyError({
1828
- message: `copyIn failed: ${e instanceof Error ? e.message : String(e)}`
1829
- })
1830
- }),
1831
- copyFileOut: (sandboxPath, hostPath) => Effect.tryPromise({
1832
- try: () => handle.copyFileOut(sandboxPath, hostPath),
1833
- catch: (e) => new CopyError({
1834
- message: `copyFileOut failed: ${e instanceof Error ? e.message : String(e)}`
1835
- })
1836
- })
1837
- });
1838
- var SANDBOX_REPO_DIR = "/home/agent/workspace";
1839
- var SandboxFactory = class extends Context.Tag("SandboxFactory")() {
1840
- };
1841
- var SandboxConfig = class extends Context.Tag("SandboxConfig")() {
1842
- };
1843
- var printWorktreePreservedMessage = (worktreePath, reason) => {
1844
- console.error(`
1845
- ${reason}`);
1846
- console.error(` To review: cd ${worktreePath}`);
1847
- console.error(` To clean up: git worktree remove --force ${worktreePath}`);
1848
- };
1849
- var cleanupWorktree = (worktreePath, exit) => hasUncommittedChanges(worktreePath).pipe(
1850
- Effect.catchAll(() => Effect.succeed(false)),
1851
- Effect.flatMap((isDirty) => {
1852
- if (isDirty) {
1853
- printWorktreePreservedMessage(
1854
- worktreePath,
1855
- Exit.isSuccess(exit) ? `Run succeeded but worktree has uncommitted changes at ${worktreePath}` : `Worktree preserved at ${worktreePath}`
1856
- );
1857
- return Effect.succeed(worktreePath);
1858
- }
1859
- if (!Exit.isSuccess(exit)) {
1860
- console.error(`
1861
- Worktree removed (no uncommitted changes)`);
1862
- }
1863
- return remove(worktreePath).pipe(
1864
- Effect.map(() => void 0)
1865
- );
1866
- })
1867
- );
1868
- var attachPreservedPath = (path, e) => {
1869
- if (path !== void 0) {
1870
- if (e instanceof AgentIdleTimeoutError) {
1871
- return new AgentIdleTimeoutError({
1872
- message: e.message,
1873
- timeoutMs: e.timeoutMs,
1874
- preservedWorktreePath: path
1875
- });
1876
- }
1877
- if (e instanceof AgentError) {
1878
- return new AgentError({
1879
- message: e.message,
1880
- preservedWorktreePath: path
1881
- });
1882
- }
1883
- }
1884
- return e;
1885
- };
1886
- var WorktreeDockerSandboxFactory = {
1887
- layer: Layer.effect(
1888
- SandboxFactory,
1889
- Effect.gen(function* () {
1890
- const {
1891
- env,
1892
- hostRepoDir,
1893
- copyToWorktree: copyPaths,
1894
- name,
1895
- sandboxProvider,
1896
- branchStrategy,
1897
- hooks,
1898
- signal,
1899
- timeouts
1900
- } = yield* SandboxConfig;
1901
- const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
1902
- const baseBranch = branchStrategy.type === "branch" ? branchStrategy.baseBranch : void 0;
1903
- const fileSystem = yield* FileSystem.FileSystem;
1904
- const pruneAndCreate = () => pruneStale(hostRepoDir).pipe(
1905
- Effect.catchAll(
1906
- (e) => Effect.sync(() => {
1907
- console.error(
1908
- "[shipyard] Warning: failed to prune stale worktrees:",
1909
- e.message
1910
- );
1911
- })
1912
- ),
1913
- Effect.andThen(
1914
- branch ? create(hostRepoDir, { branch, baseBranch }) : create(hostRepoDir, { name })
1915
- ),
1916
- Effect.provideService(FileSystem.FileSystem, fileSystem)
1917
- );
1918
- return {
1919
- withSandbox: (makeEffect) => {
1920
- {
1921
- let preservedPath;
1922
- return Effect.acquireUseRelease(
1923
- pruneAndCreate(),
1924
- (worktreeInfo) => (hooks?.host?.onWorktreeReady?.length ? runHostHooks(
1925
- hooks.host.onWorktreeReady,
1926
- worktreeInfo.path,
1927
- signal
1928
- ) : Effect.void).pipe(
1929
- Effect.andThen(
1930
- Effect.acquireUseRelease(
1931
- startSandbox({
1932
- provider: sandboxProvider,
1933
- hostRepoDir: worktreeInfo.path,
1934
- sourceRepoDir: hostRepoDir,
1935
- env,
1936
- copyPaths,
1937
- copyTimeoutMs: timeouts?.copyToWorktreeMs
1938
- }),
1939
- ({ sandbox, worktreePath, handle }) => makeEffect(
1940
- {
1941
- hostWorktreePath: worktreeInfo.path,
1942
- sandboxRepoPath: worktreePath,
1943
- sessionTransferHandle: toSessionTransferHandle(handle),
1944
- applyToHost: () => syncOut(
1945
- worktreeInfo.path,
1946
- handle
1947
- )
1948
- },
1949
- sandbox
1950
- ),
1951
- ({ handle }) => Effect.tryPromise({
1952
- try: () => handle.close(),
1953
- catch: () => void 0
1954
- }).pipe(Effect.orDie)
1955
- )
1956
- )
1957
- ),
1958
- (worktreeInfo, exit) => cleanupWorktree(worktreeInfo.path, exit).pipe(
1959
- Effect.tap((p) => {
1960
- preservedPath = p;
1961
- }),
1962
- Effect.asVoid,
1963
- Effect.orDie
1964
- )
1965
- ).pipe(
1966
- Effect.map((value) => ({
1967
- value,
1968
- preservedWorktreePath: preservedPath
1969
- })),
1970
- Effect.mapError(
1971
- (e) => attachPreservedPath(preservedPath, e)
1972
- )
1973
- );
1974
- }
1975
- }
1976
- };
1977
- })
1978
- )
1979
- };
1980
1862
  var execHostGit3 = (args, cwd) => Effect.tryPromise({
1981
1863
  try: () => execHostGit(args, cwd),
1982
1864
  catch: (e) => new SyncError({
@@ -2082,17 +1964,101 @@ var syncIn = (hostRepoDir, handle) => Effect.gen(function* () {
2082
1964
  // src/startSandbox.ts
2083
1965
  var CONTAINER_START_TIMEOUT_MS = 12e4;
2084
1966
  var SYNC_IN_TIMEOUT_MS = 12e4;
2085
- var COPY_PATHS_TIMEOUT_MS = 6e4;
2086
- var startSandbox = (options) => Effect.gen(function* () {
2087
- const handle = yield* Effect.tryPromise({
2088
- try: () => options.provider.create({
2089
- env: options.env,
2090
- hostRepoPath: options.sourceRepoDir ?? options.hostRepoDir
2091
- }),
2092
- catch: (e) => new WorktreeError({
2093
- message: `Isolated provider '${options.provider.name}' setup failed: ${e instanceof Error ? e.message : String(e)}`
2094
- })
2095
- }).pipe(
1967
+ var COPY_PATHS_TIMEOUT_MS = 12e4;
1968
+ var startSandbox = (options) => {
1969
+ if (options.provider.tag === "bind-mount") {
1970
+ return startBindMountSandbox(options);
1971
+ }
1972
+ if (options.provider.tag === "none") {
1973
+ return startNoSandbox(options);
1974
+ }
1975
+ return startIsolatedSandbox(options);
1976
+ };
1977
+ var startNoSandbox = (options) => Effect.tryPromise({
1978
+ try: () => options.provider.create({
1979
+ worktreePath: options.worktreeOrRepoPath,
1980
+ env: options.env
1981
+ }),
1982
+ catch: (e) => new WorktreeError({
1983
+ message: `Provider '${options.provider.name}' create failed: ${e instanceof Error ? e.message : String(e)}`
1984
+ })
1985
+ }).pipe(
1986
+ Effect.map((handle) => ({
1987
+ handle,
1988
+ sandbox: makeSandboxFromHandle(handle),
1989
+ worktreePath: handle.worktreePath
1990
+ }))
1991
+ );
1992
+ var startBindMountSandbox = (options) => Effect.tryPromise({
1993
+ try: () => {
1994
+ const usesRunnerMask = options.worktreeOrRepoPath === options.hostRepoDir && existsSync(join(options.hostRepoDir, CONFIG_DIR, RUNNER_DIR)) && existsSync(
1995
+ join(options.hostRepoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR)
1996
+ );
1997
+ if (usesRunnerMask) {
1998
+ assertSafeRepositoryRunnerDirectories(options.hostRepoDir);
1999
+ }
2000
+ const rawMounts = [
2001
+ {
2002
+ hostPath: options.worktreeOrRepoPath,
2003
+ sandboxPath: options.repoDir
2004
+ },
2005
+ ...options.gitMounts,
2006
+ ...usesRunnerMask ? [
2007
+ {
2008
+ hostPath: join(
2009
+ options.hostRepoDir,
2010
+ CONFIG_DIR,
2011
+ RUNNER_SANDBOX_MASK_DIR
2012
+ ),
2013
+ sandboxPath: posix.join(
2014
+ options.repoDir,
2015
+ CONFIG_DIR,
2016
+ RUNNER_DIR
2017
+ ),
2018
+ readonly: true
2019
+ }
2020
+ ] : []
2021
+ ];
2022
+ const mounts = normalizeMounts(
2023
+ rawMounts,
2024
+ options.worktreeOrRepoPath,
2025
+ SANDBOX_REPO_DIR
2026
+ );
2027
+ const worktreePath = process.platform === "win32" ? options.worktreeOrRepoPath.replace(/\\/g, "/") : options.worktreeOrRepoPath;
2028
+ return options.provider.create({
2029
+ worktreePath,
2030
+ hostRepoPath: options.hostRepoDir,
2031
+ mounts,
2032
+ env: options.env
2033
+ });
2034
+ },
2035
+ catch: (e) => new WorktreeError({
2036
+ message: `Provider '${options.provider.name}' create failed: ${e instanceof Error ? e.message : String(e)}`
2037
+ })
2038
+ }).pipe(
2039
+ Effect.map((handle) => ({
2040
+ handle,
2041
+ sandbox: makeSandboxFromHandle(handle),
2042
+ worktreePath: handle.worktreePath
2043
+ })),
2044
+ withTimeout(
2045
+ CONTAINER_START_TIMEOUT_MS,
2046
+ () => new ContainerStartTimeoutError({
2047
+ message: `Sandbox container start timed out after ${CONTAINER_START_TIMEOUT_MS}ms`,
2048
+ timeoutMs: CONTAINER_START_TIMEOUT_MS
2049
+ })
2050
+ )
2051
+ );
2052
+ var startIsolatedSandbox = (options) => Effect.gen(function* () {
2053
+ const handle = yield* Effect.tryPromise({
2054
+ try: () => options.provider.create({
2055
+ env: options.env,
2056
+ hostRepoPath: options.sourceRepoDir ?? options.hostRepoDir
2057
+ }),
2058
+ catch: (e) => new WorktreeError({
2059
+ message: `Isolated provider '${options.provider.name}' setup failed: ${e instanceof Error ? e.message : String(e)}`
2060
+ })
2061
+ }).pipe(
2096
2062
  withTimeout(
2097
2063
  CONTAINER_START_TIMEOUT_MS,
2098
2064
  () => new ContainerStartTimeoutError({
@@ -2113,8 +2079,6 @@ var startSandbox = (options) => Effect.gen(function* () {
2113
2079
  );
2114
2080
  if (options.copyPaths && options.copyPaths.length > 0) {
2115
2081
  const pathsToCopy = options.copyPaths;
2116
- const copySourceDir = options.copySourceDir ?? options.sourceRepoDir ?? options.hostRepoDir;
2117
- const copyTimeoutMs = options.copyTimeoutMs ?? COPY_PATHS_TIMEOUT_MS;
2118
2082
  yield* Effect.gen(function* () {
2119
2083
  for (const relativePath of pathsToCopy) {
2120
2084
  let hostPath;
@@ -2122,7 +2086,7 @@ var startSandbox = (options) => Effect.gen(function* () {
2122
2086
  try {
2123
2087
  assertExcludesRepositoryRunner(relativePath);
2124
2088
  hostPath = resolveSafeRelativePath(
2125
- copySourceDir,
2089
+ options.sourceRepoDir ?? options.hostRepoDir,
2126
2090
  relativePath,
2127
2091
  "copy path"
2128
2092
  );
@@ -2145,7 +2109,7 @@ var startSandbox = (options) => Effect.gen(function* () {
2145
2109
  yield* Effect.tryPromise({
2146
2110
  try: async () => {
2147
2111
  await assertNoSymlinkComponents(
2148
- copySourceDir,
2112
+ options.sourceRepoDir ?? options.hostRepoDir,
2149
2113
  hostPath,
2150
2114
  "copy source"
2151
2115
  );
@@ -2163,10 +2127,10 @@ var startSandbox = (options) => Effect.gen(function* () {
2163
2127
  }
2164
2128
  }).pipe(
2165
2129
  withTimeout(
2166
- copyTimeoutMs,
2130
+ COPY_PATHS_TIMEOUT_MS,
2167
2131
  () => new CopyToWorktreeTimeoutError({
2168
- message: `Copying paths to sandbox timed out after ${copyTimeoutMs}ms`,
2169
- timeoutMs: copyTimeoutMs,
2132
+ message: `Copying paths to worktree timed out after ${COPY_PATHS_TIMEOUT_MS}ms`,
2133
+ timeoutMs: COPY_PATHS_TIMEOUT_MS,
2170
2134
  paths: pathsToCopy
2171
2135
  })
2172
2136
  )
@@ -2180,6 +2144,667 @@ var startSandbox = (options) => Effect.gen(function* () {
2180
2144
  }).pipe(Effect.onError(() => Effect.promise(() => handle.close())));
2181
2145
  });
2182
2146
 
2183
- export { ACTIVATION_LABEL, AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CopyToWorktreeError, CopyToWorktreeTimeoutError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, RUNNER_DIR, RUNTIME_NAMESPACE, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertExcludesRepositoryRunner, assertNoSymlinkComponents, assertProtectedDirectoryIdentities, create, createIsolatedSandboxProvider, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, pruneStale, registerShutdown, remove, repositoryRunnerEnvironment, resolveCwd, resolveSafeRelativePath, runHostHooks, shellQuote, startSandbox, syncOut, toSessionTransferHandle, withSandboxLifecycle, withTimeout };
2184
- //# sourceMappingURL=chunk-L6PX5QTU.js.map
2185
- //# sourceMappingURL=chunk-L6PX5QTU.js.map
2147
+ // src/SandboxFactory.ts
2148
+ var getCopyIn = (handle) => {
2149
+ if ("copyIn" in handle) {
2150
+ return (hostPath, sandboxPath) => Effect.tryPromise({
2151
+ try: () => handle.copyIn(hostPath, sandboxPath),
2152
+ catch: (e) => new CopyError({
2153
+ message: `copyIn failed: ${e instanceof Error ? e.message : String(e)}`
2154
+ })
2155
+ });
2156
+ }
2157
+ if ("copyFileIn" in handle) {
2158
+ return (hostPath, sandboxPath) => Effect.tryPromise({
2159
+ try: () => handle.copyFileIn(hostPath, sandboxPath),
2160
+ catch: (e) => new CopyError({
2161
+ message: `copyFileIn failed: ${e instanceof Error ? e.message : String(e)}`
2162
+ })
2163
+ });
2164
+ }
2165
+ return () => Effect.fail(
2166
+ new CopyError({
2167
+ message: "copyIn is not supported for this sandbox provider"
2168
+ })
2169
+ );
2170
+ };
2171
+ var makeSandboxFromHandle = (handle) => ({
2172
+ exec: (command, options) => Effect.tryPromise({
2173
+ try: () => handle.exec(command, options),
2174
+ catch: (e) => new ExecError({
2175
+ command,
2176
+ message: `exec failed: ${e instanceof Error ? e.message : String(e)}`
2177
+ })
2178
+ }),
2179
+ copyIn: getCopyIn(handle),
2180
+ copyFileOut: "copyFileOut" in handle ? (sandboxPath, hostPath) => Effect.tryPromise({
2181
+ try: () => handle.copyFileOut(sandboxPath, hostPath),
2182
+ catch: (e) => new CopyError({
2183
+ message: `copyFileOut failed: ${e instanceof Error ? e.message : String(e)}`
2184
+ })
2185
+ }) : () => Effect.fail(
2186
+ new CopyError({
2187
+ message: "copyFileOut is not supported for this sandbox provider"
2188
+ })
2189
+ )
2190
+ });
2191
+ var SANDBOX_REPO_DIR = "/home/agent/workspace";
2192
+ var SandboxFactory = class extends Context.Tag("SandboxFactory")() {
2193
+ };
2194
+ var SandboxConfig = class extends Context.Tag("SandboxConfig")() {
2195
+ };
2196
+ var printWorktreePreservedMessage = (worktreePath, reason) => {
2197
+ console.error(`
2198
+ ${reason}`);
2199
+ console.error(` To review: cd ${worktreePath}`);
2200
+ console.error(` To clean up: git worktree remove --force ${worktreePath}`);
2201
+ };
2202
+ var cleanupWorktree = (worktreePath, exit) => hasUncommittedChanges(worktreePath).pipe(
2203
+ Effect.catchAll(() => Effect.succeed(false)),
2204
+ Effect.flatMap((isDirty) => {
2205
+ if (isDirty) {
2206
+ printWorktreePreservedMessage(
2207
+ worktreePath,
2208
+ Exit.isSuccess(exit) ? `Run succeeded but worktree has uncommitted changes at ${worktreePath}` : `Worktree preserved at ${worktreePath}`
2209
+ );
2210
+ return Effect.succeed(worktreePath);
2211
+ }
2212
+ if (!Exit.isSuccess(exit)) {
2213
+ console.error(`
2214
+ Worktree removed (no uncommitted changes)`);
2215
+ }
2216
+ return remove(worktreePath).pipe(
2217
+ Effect.map(() => void 0)
2218
+ );
2219
+ })
2220
+ );
2221
+ var attachPreservedPath = (path, e) => {
2222
+ if (path !== void 0) {
2223
+ if (e instanceof AgentIdleTimeoutError) {
2224
+ return new AgentIdleTimeoutError({
2225
+ message: e.message,
2226
+ timeoutMs: e.timeoutMs,
2227
+ preservedWorktreePath: path
2228
+ });
2229
+ }
2230
+ if (e instanceof AgentError) {
2231
+ return new AgentError({
2232
+ message: e.message,
2233
+ preservedWorktreePath: path
2234
+ });
2235
+ }
2236
+ }
2237
+ return e;
2238
+ };
2239
+ var resolveGitMounts = (gitPath) => Effect.gen(function* () {
2240
+ const fs = yield* FileSystem.FileSystem;
2241
+ const stat2 = yield* fs.stat(gitPath);
2242
+ if (stat2.type === "Directory") {
2243
+ return [{ hostPath: gitPath, sandboxPath: gitPath }];
2244
+ }
2245
+ const content = (yield* fs.readFileString(gitPath)).trim();
2246
+ const match = content.match(/^gitdir:\s*(.+)$/);
2247
+ if (!match) {
2248
+ return [{ hostPath: gitPath, sandboxPath: gitPath }];
2249
+ }
2250
+ const gitdirPath = match[1];
2251
+ const parentGitDir = resolve(gitdirPath, "..", "..");
2252
+ return [
2253
+ { hostPath: gitPath, sandboxPath: gitPath },
2254
+ { hostPath: parentGitDir, sandboxPath: parentGitDir }
2255
+ ];
2256
+ });
2257
+ var WorktreeDockerSandboxFactory = {
2258
+ layer: Layer.effect(
2259
+ SandboxFactory,
2260
+ Effect.gen(function* () {
2261
+ const {
2262
+ env,
2263
+ hostRepoDir,
2264
+ copyToWorktree: copyPaths,
2265
+ name,
2266
+ sandboxProvider,
2267
+ branchStrategy,
2268
+ hooks,
2269
+ signal,
2270
+ timeouts
2271
+ } = yield* SandboxConfig;
2272
+ const isHeadMode = branchStrategy.type === "head";
2273
+ const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
2274
+ const baseBranch = branchStrategy.type === "branch" ? branchStrategy.baseBranch : void 0;
2275
+ const fileSystem = yield* FileSystem.FileSystem;
2276
+ const display = yield* Display;
2277
+ const pruneAndCreate = () => pruneStale(hostRepoDir).pipe(
2278
+ Effect.catchAll(
2279
+ (e) => Effect.sync(() => {
2280
+ console.error(
2281
+ "[shipyard] Warning: failed to prune stale worktrees:",
2282
+ e.message
2283
+ );
2284
+ })
2285
+ ),
2286
+ Effect.andThen(
2287
+ branch ? create(hostRepoDir, { branch, baseBranch }) : create(hostRepoDir, { name })
2288
+ ),
2289
+ Effect.provideService(FileSystem.FileSystem, fileSystem)
2290
+ );
2291
+ return {
2292
+ withSandbox: (makeEffect) => {
2293
+ if (sandboxProvider.tag === "none") {
2294
+ let preservedPath;
2295
+ if (isHeadMode) {
2296
+ return (hooks?.host?.onWorktreeReady?.length ? runHostHooks(
2297
+ hooks.host.onWorktreeReady,
2298
+ hostRepoDir,
2299
+ signal
2300
+ ) : Effect.void).pipe(
2301
+ Effect.andThen(
2302
+ Effect.acquireUseRelease(
2303
+ startSandbox({
2304
+ provider: sandboxProvider,
2305
+ hostRepoDir,
2306
+ env,
2307
+ worktreeOrRepoPath: hostRepoDir
2308
+ }),
2309
+ ({ sandbox, worktreePath }) => makeEffect(
2310
+ {
2311
+ hostWorktreePath: hostRepoDir,
2312
+ sandboxRepoPath: worktreePath
2313
+ },
2314
+ sandbox
2315
+ ),
2316
+ ({ handle }) => Effect.tryPromise({
2317
+ try: () => handle.close(),
2318
+ catch: () => void 0
2319
+ }).pipe(Effect.orDie)
2320
+ ).pipe(
2321
+ Effect.map((value) => ({
2322
+ value,
2323
+ preservedWorktreePath: void 0
2324
+ }))
2325
+ )
2326
+ )
2327
+ );
2328
+ }
2329
+ return Effect.acquireUseRelease(
2330
+ pruneAndCreate(),
2331
+ (worktreeInfo) => (copyPaths && copyPaths.length > 0 ? display.spinner(
2332
+ "Copying to worktree",
2333
+ copyToWorktree(
2334
+ copyPaths,
2335
+ hostRepoDir,
2336
+ worktreeInfo.path,
2337
+ timeouts?.copyToWorktreeMs
2338
+ )
2339
+ ) : Effect.succeed(void 0)).pipe(
2340
+ Effect.andThen(
2341
+ hooks?.host?.onWorktreeReady?.length ? runHostHooks(
2342
+ hooks.host.onWorktreeReady,
2343
+ worktreeInfo.path,
2344
+ signal
2345
+ ) : Effect.void
2346
+ ),
2347
+ Effect.andThen(
2348
+ Effect.acquireUseRelease(
2349
+ startSandbox({
2350
+ provider: sandboxProvider,
2351
+ hostRepoDir,
2352
+ env,
2353
+ worktreeOrRepoPath: worktreeInfo.path
2354
+ }),
2355
+ ({ sandbox, worktreePath }) => makeEffect(
2356
+ {
2357
+ hostWorktreePath: worktreeInfo.path,
2358
+ sandboxRepoPath: worktreePath
2359
+ },
2360
+ sandbox
2361
+ ),
2362
+ ({ handle }) => Effect.tryPromise({
2363
+ try: () => handle.close(),
2364
+ catch: () => void 0
2365
+ }).pipe(Effect.orDie)
2366
+ )
2367
+ )
2368
+ ),
2369
+ (worktreeInfo, exit) => cleanupWorktree(worktreeInfo.path, exit).pipe(
2370
+ Effect.tap((p) => {
2371
+ preservedPath = p;
2372
+ }),
2373
+ Effect.asVoid,
2374
+ Effect.orDie
2375
+ )
2376
+ ).pipe(
2377
+ Effect.map((value) => ({
2378
+ value,
2379
+ preservedWorktreePath: preservedPath
2380
+ })),
2381
+ Effect.mapError(
2382
+ (e) => attachPreservedPath(preservedPath, e)
2383
+ )
2384
+ );
2385
+ }
2386
+ if (sandboxProvider.tag === "isolated") {
2387
+ let preservedPath;
2388
+ return Effect.acquireUseRelease(
2389
+ pruneAndCreate(),
2390
+ (worktreeInfo) => (hooks?.host?.onWorktreeReady?.length ? runHostHooks(
2391
+ hooks.host.onWorktreeReady,
2392
+ worktreeInfo.path,
2393
+ signal
2394
+ ) : Effect.void).pipe(
2395
+ Effect.andThen(
2396
+ Effect.acquireUseRelease(
2397
+ startSandbox({
2398
+ provider: sandboxProvider,
2399
+ hostRepoDir: worktreeInfo.path,
2400
+ sourceRepoDir: hostRepoDir,
2401
+ env,
2402
+ copyPaths
2403
+ }),
2404
+ ({ sandbox, worktreePath, handle }) => makeEffect(
2405
+ {
2406
+ hostWorktreePath: worktreeInfo.path,
2407
+ sandboxRepoPath: worktreePath,
2408
+ bindMountHandle: toSessionTransferHandle(handle),
2409
+ applyToHost: () => syncOut(
2410
+ worktreeInfo.path,
2411
+ handle
2412
+ )
2413
+ },
2414
+ sandbox
2415
+ ),
2416
+ ({ handle }) => Effect.tryPromise({
2417
+ try: () => handle.close(),
2418
+ catch: () => void 0
2419
+ }).pipe(Effect.orDie)
2420
+ )
2421
+ )
2422
+ ),
2423
+ (worktreeInfo, exit) => cleanupWorktree(worktreeInfo.path, exit).pipe(
2424
+ Effect.tap((p) => {
2425
+ preservedPath = p;
2426
+ }),
2427
+ Effect.asVoid,
2428
+ Effect.orDie
2429
+ )
2430
+ ).pipe(
2431
+ Effect.map((value) => ({
2432
+ value,
2433
+ preservedWorktreePath: preservedPath
2434
+ })),
2435
+ Effect.mapError(
2436
+ (e) => attachPreservedPath(preservedPath, e)
2437
+ )
2438
+ );
2439
+ }
2440
+ if (isHeadMode) {
2441
+ const gitPath = join(hostRepoDir, ".git");
2442
+ return (hooks?.host?.onWorktreeReady?.length ? runHostHooks(hooks.host.onWorktreeReady, hostRepoDir, signal) : Effect.void).pipe(
2443
+ Effect.andThen(resolveGitMounts(gitPath)),
2444
+ Effect.provideService(FileSystem.FileSystem, fileSystem),
2445
+ Effect.mapError(
2446
+ (e) => new WorktreeError({
2447
+ message: `Failed to resolve git mounts: ${e}`
2448
+ })
2449
+ ),
2450
+ Effect.flatMap(
2451
+ (gitMounts) => (
2452
+ // Patch git mounts for Windows worktree compatibility (ADR-0006)
2453
+ patchGitMountsForWindows(
2454
+ gitMounts,
2455
+ hostRepoDir,
2456
+ SANDBOX_REPO_DIR
2457
+ )
2458
+ )
2459
+ ),
2460
+ Effect.flatMap(
2461
+ (gitMounts) => Effect.acquireUseRelease(
2462
+ startSandbox({
2463
+ provider: sandboxProvider,
2464
+ hostRepoDir,
2465
+ env,
2466
+ worktreeOrRepoPath: hostRepoDir,
2467
+ gitMounts,
2468
+ repoDir: SANDBOX_REPO_DIR
2469
+ }),
2470
+ // Use
2471
+ ({ sandbox, worktreePath, handle }) => makeEffect(
2472
+ {
2473
+ hostWorktreePath: hostRepoDir,
2474
+ sandboxRepoPath: worktreePath,
2475
+ bindMountHandle: handle
2476
+ },
2477
+ sandbox
2478
+ ),
2479
+ // Release
2480
+ ({ handle }) => Effect.tryPromise({
2481
+ try: () => handle.close(),
2482
+ catch: () => void 0
2483
+ }).pipe(Effect.orDie)
2484
+ ).pipe(
2485
+ Effect.map((value) => ({
2486
+ value,
2487
+ preservedWorktreePath: void 0
2488
+ }))
2489
+ )
2490
+ )
2491
+ );
2492
+ }
2493
+ let preservedWorktreePath;
2494
+ return Effect.acquireUseRelease(
2495
+ // Acquire: prune stale worktrees (best-effort), then create the worktree.
2496
+ pruneAndCreate(),
2497
+ // Use: copy files, run host hooks, resolve+patch git mounts, then start
2498
+ // the sandbox under a nested acquireUseRelease.
2499
+ (worktreeInfo) => (copyPaths && copyPaths.length > 0 ? display.spinner(
2500
+ "Copying to worktree",
2501
+ copyToWorktree(
2502
+ copyPaths,
2503
+ hostRepoDir,
2504
+ worktreeInfo.path,
2505
+ timeouts?.copyToWorktreeMs
2506
+ )
2507
+ ) : Effect.succeed(void 0)).pipe(
2508
+ Effect.andThen(
2509
+ hooks?.host?.onWorktreeReady?.length ? runHostHooks(
2510
+ hooks.host.onWorktreeReady,
2511
+ worktreeInfo.path,
2512
+ signal
2513
+ ) : Effect.void
2514
+ ),
2515
+ Effect.andThen(
2516
+ resolveGitMounts(join(hostRepoDir, ".git")).pipe(
2517
+ Effect.provideService(FileSystem.FileSystem, fileSystem),
2518
+ Effect.mapError(
2519
+ (e) => new WorktreeError({
2520
+ message: `Failed to resolve git mounts: ${e}`
2521
+ })
2522
+ )
2523
+ )
2524
+ ),
2525
+ // Patch git mounts for Windows worktree compatibility (ADR-0006)
2526
+ Effect.flatMap(
2527
+ (gitMounts) => patchGitMountsForWindows(
2528
+ gitMounts,
2529
+ worktreeInfo.path,
2530
+ SANDBOX_REPO_DIR
2531
+ )
2532
+ ),
2533
+ Effect.flatMap(
2534
+ (gitMounts) => Effect.acquireUseRelease(
2535
+ // sandboxProvider is guaranteed bind-mount here
2536
+ // (isolated providers return early above)
2537
+ startSandbox({
2538
+ provider: sandboxProvider,
2539
+ hostRepoDir,
2540
+ env,
2541
+ worktreeOrRepoPath: worktreeInfo.path,
2542
+ gitMounts,
2543
+ repoDir: SANDBOX_REPO_DIR
2544
+ }),
2545
+ ({ sandbox, worktreePath, handle }) => makeEffect(
2546
+ {
2547
+ hostWorktreePath: worktreeInfo.path,
2548
+ sandboxRepoPath: worktreePath,
2549
+ bindMountHandle: handle
2550
+ },
2551
+ sandbox
2552
+ ),
2553
+ ({ handle }) => Effect.tryPromise({
2554
+ try: () => handle.close(),
2555
+ catch: () => void 0
2556
+ }).pipe(Effect.orDie)
2557
+ )
2558
+ )
2559
+ ),
2560
+ // Release: remove or preserve the worktree based on dirty state.
2561
+ (worktreeInfo, exit) => cleanupWorktree(worktreeInfo.path, exit).pipe(
2562
+ Effect.tap((p) => {
2563
+ preservedWorktreePath = p;
2564
+ }),
2565
+ Effect.asVoid,
2566
+ Effect.orDie
2567
+ )
2568
+ ).pipe(
2569
+ Effect.map((value) => ({
2570
+ value,
2571
+ preservedWorktreePath
2572
+ })),
2573
+ Effect.mapError(
2574
+ (e) => attachPreservedPath(preservedWorktreePath, e)
2575
+ )
2576
+ );
2577
+ }
2578
+ };
2579
+ })
2580
+ )
2581
+ };
2582
+
2583
+ // src/mountUtils.ts
2584
+ var defaultImageName = (repoDir) => {
2585
+ const dirName = repoDir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "local";
2586
+ const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
2587
+ return `${RUNTIME_NAMESPACE}:${sanitized || "local"}`;
2588
+ };
2589
+ var expandTilde = (p, homeDirPath) => {
2590
+ const home = homeDirPath ?? homedir();
2591
+ if (p === "~") return home;
2592
+ if (p.startsWith("~/") || p.startsWith("~\\")) return home + "/" + p.slice(2);
2593
+ return p;
2594
+ };
2595
+ var resolveHostPath = (hostPath) => {
2596
+ const expanded = expandTilde(hostPath);
2597
+ return isAbsolute(expanded) ? expanded : resolve(process.cwd(), expanded);
2598
+ };
2599
+ var resolveSandboxPath = (sandboxPath, sandboxHomedir) => {
2600
+ const hasTilde = sandboxPath === "~" || sandboxPath.startsWith("~/") || sandboxPath.startsWith("~\\");
2601
+ if (hasTilde && sandboxHomedir === void 0) {
2602
+ throw new Error(
2603
+ `sandboxPath "${sandboxPath}" contains a tilde but the provider has no sandboxHomedir set`
2604
+ );
2605
+ }
2606
+ const expanded = hasTilde ? expandTilde(sandboxPath, sandboxHomedir) : sandboxPath;
2607
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(SANDBOX_REPO_DIR, expanded);
2608
+ };
2609
+ var resolveUserMounts = (mounts, sandboxHomedir) => mounts.map((m) => {
2610
+ const resolvedHostPath = resolveHostPath(m.hostPath);
2611
+ if (!existsSync(resolvedHostPath)) {
2612
+ throw new Error(
2613
+ `Mount hostPath does not exist: ${m.hostPath}` + (m.hostPath !== resolvedHostPath ? ` (resolved to ${resolvedHostPath})` : "")
2614
+ );
2615
+ }
2616
+ return {
2617
+ hostPath: resolvedHostPath,
2618
+ sandboxPath: resolveSandboxPath(m.sandboxPath, sandboxHomedir),
2619
+ ...m.readonly ? { readonly: true } : {}
2620
+ };
2621
+ });
2622
+ var normalizeMounts = (mounts, worktreeHostPath, sandboxRepoDir, platform = process.platform) => {
2623
+ if (platform !== "win32") return mounts;
2624
+ const normalizedWorktree = worktreeHostPath.replace(/\\/g, "/");
2625
+ return mounts.map((m) => {
2626
+ const hostPath = m.hostPath.replace(/\\/g, "/");
2627
+ let sandboxPath = m.sandboxPath;
2628
+ if (/^[A-Za-z]:[/\\]/.test(sandboxPath) || sandboxPath.includes("\\")) {
2629
+ const normalizedSandboxPath = sandboxPath.replace(/\\/g, "/");
2630
+ if (normalizedSandboxPath.startsWith(normalizedWorktree + "/")) {
2631
+ const relativeSuffix = normalizedSandboxPath.slice(
2632
+ normalizedWorktree.length
2633
+ );
2634
+ sandboxPath = sandboxRepoDir + relativeSuffix;
2635
+ } else if (normalizedSandboxPath === normalizedWorktree) {
2636
+ sandboxPath = sandboxRepoDir;
2637
+ } else {
2638
+ sandboxPath = normalizedSandboxPath;
2639
+ }
2640
+ }
2641
+ return { ...m, hostPath, sandboxPath };
2642
+ });
2643
+ };
2644
+ var parseGitdirPath = (gitdirPath) => {
2645
+ const normalized = gitdirPath.replace(/\\/g, "/").replace(/\/+$/, "");
2646
+ const segments = normalized.split("/");
2647
+ const worktreeName = segments.pop();
2648
+ segments.pop();
2649
+ const parentGitDir = segments.join("/");
2650
+ return { worktreeName, parentGitDir };
2651
+ };
2652
+ var patchGitMountsForWindows = (gitMounts, worktreeHostPath, sandboxRepoDir, readFile2, statFile, platform = process.platform) => Effect.gen(function* () {
2653
+ if (platform !== "win32") return gitMounts;
2654
+ const _readFile = readFile2 ?? (async (p) => {
2655
+ const { readFile: rf } = await import('fs/promises');
2656
+ return rf(p, "utf-8");
2657
+ });
2658
+ const _statFile = statFile ?? (async (p) => {
2659
+ const { stat: stat2 } = await import('fs/promises');
2660
+ const s = await stat2(p);
2661
+ return s.isDirectory() ? "directory" : "file";
2662
+ });
2663
+ const gitEntryPath = join(worktreeHostPath, ".git");
2664
+ const gitEntryType = yield* Effect.tryPromise({
2665
+ try: () => _statFile(gitEntryPath),
2666
+ catch: () => null
2667
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)));
2668
+ if (gitEntryType === null) return gitMounts;
2669
+ if (gitEntryType === "directory") return gitMounts;
2670
+ const content = yield* Effect.tryPromise({
2671
+ try: () => _readFile(gitEntryPath),
2672
+ catch: () => null
2673
+ }).pipe(
2674
+ Effect.map((s) => s.trim()),
2675
+ Effect.catchAll(() => Effect.succeed(null))
2676
+ );
2677
+ if (content === null) return gitMounts;
2678
+ const match = content.match(/^gitdir:\s*(.+)$/);
2679
+ if (!match) return gitMounts;
2680
+ const gitdirPath = match[1];
2681
+ const { parentGitDir, worktreeName } = parseGitdirPath(gitdirPath);
2682
+ const correctedGitdir = `${PARENT_GIT_SANDBOX_DIR}/worktrees/${worktreeName}`;
2683
+ const tempDir = yield* Effect.tryPromise({
2684
+ try: () => mkdtemp(join(tmpdir(), `${CONTAINER_NAME_PREFIX}git-`)),
2685
+ catch: (e) => new WorktreeError({
2686
+ message: `Failed to create temp dir for git override: ${e instanceof Error ? e.message : String(e)}`
2687
+ })
2688
+ });
2689
+ const tempGitFile = join(tempDir, "git-override");
2690
+ yield* Effect.tryPromise({
2691
+ try: () => writeFile(tempGitFile, `gitdir: ${correctedGitdir}
2692
+ `),
2693
+ catch: (e) => new WorktreeError({
2694
+ message: `Failed to write git override file: ${e instanceof Error ? e.message : String(e)}`
2695
+ })
2696
+ });
2697
+ const normalizedParentGitDir = parentGitDir.replace(/\\/g, "/");
2698
+ const gitFileHostPath = gitEntryPath.replace(/\\/g, "/");
2699
+ const correctedMounts = [];
2700
+ let replacedGitFile = false;
2701
+ for (const m of gitMounts) {
2702
+ const normalizedHostPath = m.hostPath.replace(/\\/g, "/");
2703
+ if (normalizedHostPath === normalizedParentGitDir) {
2704
+ correctedMounts.push({ ...m, sandboxPath: PARENT_GIT_SANDBOX_DIR });
2705
+ } else if (normalizedHostPath === gitFileHostPath) {
2706
+ correctedMounts.push({
2707
+ ...m,
2708
+ hostPath: tempGitFile,
2709
+ sandboxPath: `${sandboxRepoDir}/.git`
2710
+ });
2711
+ replacedGitFile = true;
2712
+ } else {
2713
+ correctedMounts.push(m);
2714
+ }
2715
+ }
2716
+ if (!replacedGitFile) {
2717
+ correctedMounts.push({
2718
+ hostPath: tempGitFile,
2719
+ sandboxPath: `${sandboxRepoDir}/.git`
2720
+ });
2721
+ }
2722
+ return correctedMounts;
2723
+ });
2724
+ var formatVolumeMount = (mount, selinuxLabel) => {
2725
+ const base = `${mount.hostPath}:${mount.sandboxPath}`;
2726
+ const options = [mount.readonly ? "ro" : void 0, selinuxLabel || void 0].filter((option) => option !== void 0).join(",");
2727
+ return options ? `${base}:${options}` : base;
2728
+ };
2729
+ var processFileMountParents = (mounts, sandboxHomedir, statFn = statSync) => {
2730
+ const parentDirs = /* @__PURE__ */ new Set();
2731
+ for (const mount of mounts) {
2732
+ let isFile;
2733
+ try {
2734
+ isFile = statFn(mount.hostPath).isFile();
2735
+ } catch {
2736
+ continue;
2737
+ }
2738
+ if (!isFile) continue;
2739
+ const parentDir = resolve(dirname(mount.sandboxPath));
2740
+ const homeDir = resolve(sandboxHomedir);
2741
+ if (parentDir === homeDir) continue;
2742
+ const fromHome = relative(homeDir, parentDir);
2743
+ if (fromHome === ".." || fromHome.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(fromHome)) {
2744
+ throw new Error(
2745
+ `Cannot mount file to '${mount.sandboxPath}': parent directory '${parentDir}' is outside the sandbox home directory ('${homeDir}'). Mount the parent directory instead, or rebuild the image with '${parentDir}' pre-created.`
2746
+ );
2747
+ }
2748
+ parentDirs.add(parentDir);
2749
+ }
2750
+ return [...parentDirs];
2751
+ };
2752
+ var assertIsolatedWorkspaceMounts = (mounts) => {
2753
+ for (const mount of mounts) {
2754
+ const path = posix.resolve(mount.sandboxPath);
2755
+ if (path === "/" || path === SANDBOX_REPO_DIR || path.startsWith(`${SANDBOX_REPO_DIR}/`) || SANDBOX_REPO_DIR.startsWith(`${path}/`)) {
2756
+ throw new Error(
2757
+ `Mount overlaps the isolated workspace: ${mount.sandboxPath}. Mount outside ${SANDBOX_REPO_DIR} or use copyToWorktree.`
2758
+ );
2759
+ }
2760
+ }
2761
+ };
2762
+
2763
+ // src/shutdownRegistry.ts
2764
+ var teardownCallbacks = /* @__PURE__ */ new Set();
2765
+ var listenersInstalled = false;
2766
+ var runTeardowns = () => {
2767
+ for (const teardown of teardownCallbacks) {
2768
+ try {
2769
+ teardown();
2770
+ } catch {
2771
+ }
2772
+ }
2773
+ };
2774
+ var handleExit = () => {
2775
+ runTeardowns();
2776
+ };
2777
+ var handleSignal = () => {
2778
+ detachListeners();
2779
+ runTeardowns();
2780
+ process.exit(1);
2781
+ };
2782
+ var attachListeners = () => {
2783
+ if (listenersInstalled) return;
2784
+ listenersInstalled = true;
2785
+ process.on("exit", handleExit);
2786
+ process.on("SIGINT", handleSignal);
2787
+ process.on("SIGTERM", handleSignal);
2788
+ };
2789
+ var detachListeners = () => {
2790
+ if (!listenersInstalled) return;
2791
+ listenersInstalled = false;
2792
+ process.removeListener("exit", handleExit);
2793
+ process.removeListener("SIGINT", handleSignal);
2794
+ process.removeListener("SIGTERM", handleSignal);
2795
+ };
2796
+ var registerShutdown = (teardown) => {
2797
+ teardownCallbacks.add(teardown);
2798
+ attachListeners();
2799
+ let active = true;
2800
+ return () => {
2801
+ if (!active) return;
2802
+ active = false;
2803
+ teardownCallbacks.delete(teardown);
2804
+ if (teardownCallbacks.size === 0) detachListeners();
2805
+ };
2806
+ };
2807
+
2808
+ export { ACTIVATION_LABEL, AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertIsolatedWorkspaceMounts, assertNoSymlinkComponents, assertProtectedDirectoryIdentities, copyToWorktree, create, defaultImageName, formatVolumeMount, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, patchGitMountsForWindows, processFileMountParents, pruneStale, registerShutdown, remove, repositoryRunnerEnvironment, resolveCwd, resolveGitMounts, resolveUserMounts, runHostHooks, startSandbox, syncOut, withSandboxLifecycle };
2809
+ //# sourceMappingURL=chunk-FDYOTN55.js.map
2810
+ //# sourceMappingURL=chunk-FDYOTN55.js.map