@snappedly-tools/shipyard 0.6.0 → 0.7.0-staging.36081473933

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 +64 -165
  2. package/dist/MountConfig-BHnKnA4h.d.ts +145 -0
  3. package/dist/{chunk-WAXEMZUV.js → chunk-44I2BL6E.js} +225 -33
  4. package/dist/chunk-44I2BL6E.js.map +1 -0
  5. package/dist/{chunk-56TDSWFU.js → chunk-JI3HDDMS.js} +3 -3
  6. package/dist/{chunk-56TDSWFU.js.map → chunk-JI3HDDMS.js.map} +1 -1
  7. package/dist/{chunk-FDYOTN55.js → chunk-L6PX5QTU.js} +253 -878
  8. package/dist/chunk-L6PX5QTU.js.map +1 -0
  9. package/dist/index.d.ts +117 -97
  10. package/dist/index.js +322 -344
  11. package/dist/index.js.map +1 -1
  12. package/dist/main.js +235 -139
  13. package/dist/main.js.map +1 -1
  14. package/dist/sandboxes/docker.d.ts +1 -3
  15. package/dist/sandboxes/docker.js +2 -4
  16. package/dist/templates/parallel-planner/main.mts +67 -9
  17. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  18. package/dist/templates/parallel-planner-with-review/main.mts +70 -12
  19. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  20. package/dist/templates/sequential-reviewer/main.mts +52 -3
  21. package/dist/templates/simple-loop/main.mts +51 -2
  22. package/package.json +1 -17
  23. package/dist/MountConfig-bZoCs4Dd.d.ts +0 -26
  24. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  25. package/dist/chunk-ACD46ZM4.js +0 -136
  26. package/dist/chunk-ACD46ZM4.js.map +0 -1
  27. package/dist/chunk-FDYOTN55.js.map +0 -1
  28. package/dist/chunk-KMGNFXKN.js +0 -38
  29. package/dist/chunk-KMGNFXKN.js.map +0 -1
  30. package/dist/chunk-SOJTAJTF.js +0 -78
  31. package/dist/chunk-SOJTAJTF.js.map +0 -1
  32. package/dist/chunk-WAXEMZUV.js.map +0 -1
  33. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  34. package/dist/sandboxes/no-sandbox.js +0 -4
  35. package/dist/sandboxes/no-sandbox.js.map +0 -1
  36. package/dist/sandboxes/vercel.d.ts +0 -104
  37. package/dist/sandboxes/vercel.js +0 -166
  38. package/dist/sandboxes/vercel.js.map +0 -1
  39. package/dist/templates/blank/main.mts +0 -13
  40. package/dist/templates/blank/prompt.md +0 -12
  41. package/dist/templates/blank/template.json +0 -4
@@ -1,15 +1,15 @@
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';
1
+ import { existsSync, constants } from 'fs';
2
+ import { writeFile, chmod, mkdir, stat, copyFile, rm, readdir, mkdtemp, lstat, readFile, open } from 'fs/promises';
4
3
  import * as clack from '@clack/prompts';
5
4
  import { FileSystem } from '@effect/platform';
6
- import { join, basename, posix, dirname, resolve, normalize, relative, sep, isAbsolute } from 'path';
5
+ import { posix, join, basename, dirname, resolve, relative, sep, isAbsolute, normalize } from 'path';
7
6
  import { Context, Layer, Effect, Data, Exit, Duration, Option, Ref, Deferred, Schedule } from 'effect';
8
7
  import { styleText, promisify } from 'util';
9
- import { tmpdir, homedir } from 'os';
10
8
  import { randomBytes } from 'crypto';
11
9
  import { exec, execFile } from 'child_process';
10
+ import { tmpdir } from 'os';
12
11
 
12
+ // src/Display.ts
13
13
  var Display = class extends Context.Tag("Display")() {
14
14
  };
15
15
  var SilentDisplay = {
@@ -259,6 +259,67 @@ 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
+ };
262
323
  var CwdError = class extends Data.TaggedError("CwdError") {
263
324
  };
264
325
  var resolveCwd = (cwd) => Effect.gen(function* () {
@@ -280,8 +341,6 @@ var resolveCwd = (cwd) => Effect.gen(function* () {
280
341
  }
281
342
  return resolved;
282
343
  });
283
-
284
- // src/errors.ts
285
344
  var ExecError = class extends Data.TaggedError("ExecError") {
286
345
  };
287
346
  var ExecHostError = class extends Data.TaggedError("ExecHostError") {
@@ -364,10 +423,7 @@ var WORKTREES_DIR = "worktrees";
364
423
  var PATCHES_DIR = "patches";
365
424
  var LOCKS_DIR = "locks";
366
425
  var RUNNER_DIR = "runner";
367
- var RUNNER_SANDBOX_MASK_DIR = "runner-sandbox-mask";
368
- var PARENT_GIT_SANDBOX_DIR = "/.shipyard-parent-git";
369
426
  var SYNC_BASE_REF = "refs/shipyard/sync-base";
370
- var CONTAINER_NAME_PREFIX = "shipyard-";
371
427
  var WINDOWS_DRIVE_PATH = /^[A-Za-z]:/;
372
428
  var assertSafeRelativePath = (relativePath, label = "relative path") => {
373
429
  if (relativePath.length === 0 || relativePath.includes("\0") || isAbsolute(relativePath) || WINDOWS_DRIVE_PATH.test(relativePath) || relativePath.startsWith("\\\\") || relativePath.split(/[\\/]/).some((part) => part === "..")) {
@@ -440,7 +496,7 @@ var SAFE_CONFIG = [
440
496
  ["checkout.recurseSubmodules", "false"]
441
497
  ];
442
498
  var COMMAND_CONFIG_PATTERN = "^(filter\\..*\\.(clean|smudge|process|required)|merge\\..*\\.driver|diff\\..*\\.(command|textconv)|difftool\\..*\\.cmd|mergetool\\..*\\.cmd)$";
443
- var runGit = (args, cwd, options = {}) => new Promise((resolve6, reject) => {
499
+ var runGit = (args, cwd, options = {}) => new Promise((resolve4, reject) => {
444
500
  execFile(
445
501
  "git",
446
502
  [...args],
@@ -458,11 +514,11 @@ var runGit = (args, cwd, options = {}) => new Promise((resolve6, reject) => {
458
514
  },
459
515
  (error, stdout, stderr) => {
460
516
  if (!error) {
461
- resolve6({ stdout, stderr, exitCode: 0 });
517
+ resolve4({ stdout, stderr, exitCode: 0 });
462
518
  return;
463
519
  }
464
520
  if (typeof error.code === "number") {
465
- resolve6({ stdout, stderr, exitCode: error.code });
521
+ resolve4({ stdout, stderr, exitCode: error.code });
466
522
  return;
467
523
  }
468
524
  reject(error);
@@ -869,6 +925,14 @@ var pruneStale = (repoDir) => Effect.gen(function* () {
869
925
  })
870
926
  )
871
927
  );
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
+ };
872
936
  var PROTECTED_RUNNER_PATH = `${CONFIG_DIR}/${RUNNER_DIR}`;
873
937
  var pathKey = (value) => value.normalize("NFC").toLocaleLowerCase("en-US");
874
938
  var SAFE_RUNNER_ENV_KEYS = /* @__PURE__ */ new Set([
@@ -900,121 +964,14 @@ var assertExcludesRepositoryRunner = (relativePath) => {
900
964
  );
901
965
  }
902
966
  };
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) => {
967
+ var assertProtectedDirectoryIdentities = async (runnerDir, inspect) => {
924
968
  if (!inspect) return;
925
969
  const runner = await inspect(runnerDir);
926
- const mask = await inspect(maskDir);
927
970
  if (!runner.directory || runner.symbolicLink) {
928
971
  throw new Error(
929
972
  `The protected repository runner path must be a real directory: ${runnerDir}`
930
973
  );
931
974
  }
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
- );
1018
975
  };
1019
976
 
1020
977
  // src/RecoveryMessage.ts
@@ -1524,8 +1481,6 @@ ${msg}`);
1524
1481
  });
1525
1482
  }
1526
1483
  });
1527
-
1528
- // src/SandboxLifecycle.ts
1529
1484
  var GIT_SETUP_TIMEOUT_MS = 1e4;
1530
1485
  var HOOK_TIMEOUT_MS = 6e4;
1531
1486
  var COMMIT_COLLECTION_TIMEOUT_MS = 3e4;
@@ -1859,6 +1814,169 @@ ${err instanceof Error ? err.message : String(err)}`
1859
1814
  }
1860
1815
  return { result, branch: finalBranch, commits };
1861
1816
  });
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
+ };
1862
1980
  var execHostGit3 = (args, cwd) => Effect.tryPromise({
1863
1981
  try: () => execHostGit(args, cwd),
1864
1982
  catch: (e) => new SyncError({
@@ -1964,92 +2082,8 @@ var syncIn = (hostRepoDir, handle) => Effect.gen(function* () {
1964
2082
  // src/startSandbox.ts
1965
2083
  var CONTAINER_START_TIMEOUT_MS = 12e4;
1966
2084
  var SYNC_IN_TIMEOUT_MS = 12e4;
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* () {
2085
+ var COPY_PATHS_TIMEOUT_MS = 6e4;
2086
+ var startSandbox = (options) => Effect.gen(function* () {
2053
2087
  const handle = yield* Effect.tryPromise({
2054
2088
  try: () => options.provider.create({
2055
2089
  env: options.env,
@@ -2079,6 +2113,8 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
2079
2113
  );
2080
2114
  if (options.copyPaths && options.copyPaths.length > 0) {
2081
2115
  const pathsToCopy = options.copyPaths;
2116
+ const copySourceDir = options.copySourceDir ?? options.sourceRepoDir ?? options.hostRepoDir;
2117
+ const copyTimeoutMs = options.copyTimeoutMs ?? COPY_PATHS_TIMEOUT_MS;
2082
2118
  yield* Effect.gen(function* () {
2083
2119
  for (const relativePath of pathsToCopy) {
2084
2120
  let hostPath;
@@ -2086,7 +2122,7 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
2086
2122
  try {
2087
2123
  assertExcludesRepositoryRunner(relativePath);
2088
2124
  hostPath = resolveSafeRelativePath(
2089
- options.sourceRepoDir ?? options.hostRepoDir,
2125
+ copySourceDir,
2090
2126
  relativePath,
2091
2127
  "copy path"
2092
2128
  );
@@ -2109,7 +2145,7 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
2109
2145
  yield* Effect.tryPromise({
2110
2146
  try: async () => {
2111
2147
  await assertNoSymlinkComponents(
2112
- options.sourceRepoDir ?? options.hostRepoDir,
2148
+ copySourceDir,
2113
2149
  hostPath,
2114
2150
  "copy source"
2115
2151
  );
@@ -2127,10 +2163,10 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
2127
2163
  }
2128
2164
  }).pipe(
2129
2165
  withTimeout(
2130
- COPY_PATHS_TIMEOUT_MS,
2166
+ copyTimeoutMs,
2131
2167
  () => new CopyToWorktreeTimeoutError({
2132
- message: `Copying paths to worktree timed out after ${COPY_PATHS_TIMEOUT_MS}ms`,
2133
- timeoutMs: COPY_PATHS_TIMEOUT_MS,
2168
+ message: `Copying paths to sandbox timed out after ${copyTimeoutMs}ms`,
2169
+ timeoutMs: copyTimeoutMs,
2134
2170
  paths: pathsToCopy
2135
2171
  })
2136
2172
  )
@@ -2144,667 +2180,6 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
2144
2180
  }).pipe(Effect.onError(() => Effect.promise(() => handle.close())));
2145
2181
  });
2146
2182
 
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
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