@snappedly-tools/shipyard 0.7.0 → 0.8.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 +34 -41
  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
package/README.md CHANGED
@@ -23,78 +23,71 @@ a commit or pull request you can inspect.
23
23
 
24
24
  ## Try it on one task
25
25
 
26
- You need **Node.js 20.18.1+**, **Git**, **Docker running**, and **Codex or Claude
27
- Code** with a login or API key. Start with a clean Git repository you want
28
- Shipyard to change. If it is on GitHub, sign in with the
29
- [GitHub CLI](https://cli.github.com/) (`gh auth login`) before `init`; Shipyard
30
- creates its issue labels there.
26
+ You need **Node.js 20.18.1+**, **Git**, **Docker running**, and **Codex or Claude Code**
27
+ with a subscription login or API key.
31
28
 
32
29
  Run:
33
30
 
34
31
  ```sh
35
- git switch -c try-shipyard
36
32
  npx skills add snappedly/skills
37
- npm install --save-dev @snappedly-tools/shipyard
38
- npx shipyard init
39
33
  ```
40
34
 
41
- Ask your coding agent to run `setup-snappedly-skills` in that repository. During
42
- `shipyard init`, choose your agent, **Docker**, and the **blank** template. Follow
43
- the authentication prompts. You can skip the optional repository runner for
44
- this first task.
35
+ Ask your coding agent to run `setup-snappedly-skills` in that repository.
45
36
 
46
- Open `.shipyard/prompt.md` and describe a small change. For example:
47
-
48
- ```md
49
- # Task
37
+ ```sh
38
+ npm install --save-dev @snappedly-tools/shipyard
39
+ npx shipyard init
40
+ ```
50
41
 
51
- Document how to run this project locally using commands already in the repo.
52
- Run the relevant checks and commit the change.
42
+ During `shipyard init`, choose your agent, **Docker**, the template you want, and install the optional
43
+ [repository runner](docs/content/docs/repository-runner.mdx). Follow
44
+ the authentication prompts.
53
45
 
54
- # Done
46
+ On a local Mac, the repository runner automatically wakes Shipyard when you label an issue.
47
+ It runs as long as its terminal stays open.
55
48
 
56
- Output <promise>COMPLETE</promise> when finished.
57
- ```
49
+ See the [getting started guide](docs/content/docs/index.mdx) for authentication and
50
+ branch options.
58
51
 
59
- Fill in any credentials requested in `.shipyard/.env`. Commit the setup and
60
- prompt so the sandbox can read them. The generated Git ignore file keeps
52
+ Fill in any credentials required in `.shipyard/.env`.
53
+ `GH_TOKEN` in `.shipyard/.env` to a GH token with Contents, Issues, and Pull
54
+ Requests read/write access and Metadata read access.
55
+ Set `SHIPYARD_ROUTINE_MODEL` and `SHIPYARD_STRONG_MODEL` in `.shipyard/.env`
56
+ for the roles your template uses. `simple-loop` uses routine for triage and
57
+ implementation. `sequential-reviewer` also uses strong for issue reviews.
58
+ Parallel planner templates use routine for ticket work and strong for planning,
59
+ conflict resolution, and integration. The review-enabled planner also uses
60
+ strong for ticket and final specification reviews. See
61
+ [agent setup](docs/content/docs/agents.mdx).
62
+
63
+ Commit the setup and prompt so the sandbox can read them. Make sure the Git ignore file keeps
61
64
  `.shipyard/.env` out of the commit.
62
65
 
63
66
  ```sh
64
- git add .
65
- git commit -m "Set up Shipyard"
66
67
  npx shipyard run
67
- git show --stat HEAD
68
68
  ```
69
69
 
70
- Shipyard builds the Docker image, runs the agent, and returns its commit to your
71
- current branch. `git show` lets you inspect what changed. See the
72
- [getting started guide](docs/content/docs/index.mdx) for authentication and
73
- branch options.
70
+ Or for the automated runner:
71
+
72
+ ```sh
73
+ npx shipyard runner start
74
+ ```
74
75
 
75
76
  ## Turn GitHub issues into pull requests
76
77
 
77
- For a repository you want to automate from GitHub Issues, choose
78
- **sequential-reviewer** instead of **blank** during `shipyard init`. Set
79
- `GH_TOKEN` in `.shipyard/.env` to a token with Contents, Issues, and Pull
80
- requests read/write access and Metadata read access. Then:
78
+ Then:
81
79
 
82
80
  1. Write an issue with a clear goal and add the `shipyard` and
83
81
  `ready-for-agent` labels.
84
- 2. Run `npx shipyard run`. Shipyard checks the issue, implements it in Docker,
82
+ 2. Make sure the runner is running, or start `npx shipyard run`. Shipyard checks the issue, implements it in Docker,
85
83
  reviews the work, and opens a pull request.
86
84
  3. Inspect the pull request and merge it when you are happy with the result.
87
85
 
88
- On an Apple Silicon Mac, the optional [repository runner](docs/content/docs/repository-runner.mdx)
89
- can wake Shipyard when you label an issue. It runs while its terminal stays open.
90
- The guide covers installation, retries, and issue status labels.
91
-
92
86
  ## Choose a workflow
93
87
 
94
88
  | Template | What it does |
95
89
  | ------------------------------ | -------------------------------------------------- |
96
- | `blank` | Runs your own task and prompt. |
97
- | `simple-loop` | Works through labelled issues one at a time. |
90
+ | `simple-loop` | Works through labeled issues one at a time. |
98
91
  | `sequential-reviewer` | Implements and reviews issues before PR handoff. |
99
92
  | `parallel-planner` | Plans and works on independent issues in parallel. |
100
93
  | `parallel-planner-with-review` | Adds review to the parallel workflow. |
@@ -0,0 +1,145 @@
1
+ /** Result of executing a command inside a sandbox. */
2
+ interface ExecResult {
3
+ /** Collected standard output. */
4
+ readonly stdout: string;
5
+ /** Collected standard error. */
6
+ readonly stderr: string;
7
+ /** Process exit status. */
8
+ readonly exitCode: number;
9
+ }
10
+ /** Streams supplied when launching an interactive agent process. */
11
+ interface InteractiveExecOptions {
12
+ /** Input stream forwarded to the agent process. */
13
+ readonly stdin: NodeJS.ReadableStream;
14
+ /** Output stream receiving agent standard output. */
15
+ readonly stdout: NodeJS.WritableStream;
16
+ /** Output stream receiving agent standard error. */
17
+ readonly stderr: NodeJS.WritableStream;
18
+ /** Working directory inside Docker. */
19
+ readonly cwd?: string;
20
+ /** Abort signal that terminates the interactive process. */
21
+ readonly signal?: AbortSignal;
22
+ }
23
+ /** Handle to a running isolated sandbox. Docker implements this contract. */
24
+ interface IsolatedSandboxHandle {
25
+ /** Absolute Git workspace path inside Docker. */
26
+ readonly worktreePath: string;
27
+ /**
28
+ * Execute a command in Docker. Deliver `onLine` as output arrives so idle
29
+ * timeouts and live feedback work; buffered delivery does not satisfy this
30
+ * contract. Pipe `stdin` to the child rather than passing it as an argument.
31
+ */
32
+ exec(command: string, options?: {
33
+ /** Live output callback. */
34
+ onLine?: (line: string) => void;
35
+ /** Working directory inside Docker. */
36
+ cwd?: string;
37
+ /** Execute as root when supported. */
38
+ sudo?: boolean;
39
+ /** Input piped to the child process. */
40
+ stdin?: string;
41
+ /** Abort signal that terminates the command. */
42
+ signal?: AbortSignal;
43
+ /** Maximum combined output bytes before termination. */
44
+ maxOutputBytes?: number;
45
+ }): Promise<ExecResult>;
46
+ /** Launch an interactive process and terminate it when `signal` aborts. */
47
+ interactiveExec?(args: string[], options: InteractiveExecOptions): Promise<{
48
+ exitCode: number;
49
+ }>;
50
+ /** Copy a file or directory from the host into Docker. */
51
+ copyIn(hostPath: string, sandboxPath: string): Promise<void>;
52
+ /** Copy a file from Docker to the host. */
53
+ copyFileOut(sandboxPath: string, hostPath: string): Promise<void>;
54
+ /** Tear down the container. */
55
+ close(): Promise<void>;
56
+ }
57
+ /** File transfer shape used by agent session storage. */
58
+ interface SessionTransferHandle {
59
+ /** Absolute Git workspace path inside Docker. */
60
+ readonly worktreePath: string;
61
+ /** Run a command in the sandbox. */
62
+ exec: IsolatedSandboxHandle["exec"];
63
+ /** Launch an interactive command when available. */
64
+ interactiveExec?: IsolatedSandboxHandle["interactiveExec"];
65
+ /** Copy one host file into the sandbox. */
66
+ copyFileIn(hostPath: string, sandboxPath: string): Promise<void>;
67
+ /** Copy one sandbox file to the host. */
68
+ copyFileOut(sandboxPath: string, hostPath: string): Promise<void>;
69
+ /** Tear down the sandbox. */
70
+ close(): Promise<void>;
71
+ }
72
+ /** Inputs to Docker sandbox creation. */
73
+ interface IsolatedCreateOptions {
74
+ /** Original repository path used to derive the image name; never mounted. */
75
+ readonly hostRepoPath?: string;
76
+ /** Environment injected into the Docker container. */
77
+ readonly env: Record<string, string>;
78
+ }
79
+ /** Configuration for the isolated Docker provider contract. */
80
+ interface IsolatedSandboxProviderConfig {
81
+ /** Human-readable provider name. */
82
+ readonly name: string;
83
+ /** Provider environment merged into sandbox startup. */
84
+ readonly env?: Record<string, string>;
85
+ /** Start a sandbox and return its command and file-transfer handle. */
86
+ readonly create: (options: IsolatedCreateOptions) => Promise<IsolatedSandboxHandle>;
87
+ }
88
+ /** Docker's isolated filesystem provider contract. */
89
+ interface IsolatedSandboxProvider {
90
+ /** Isolated filesystem discriminator. */
91
+ readonly tag: "isolated";
92
+ /** Human-readable provider name. */
93
+ readonly name: string;
94
+ /** Environment variables injected into Docker. */
95
+ readonly env: Record<string, string>;
96
+ /** Start a sandbox. */
97
+ readonly create: (options: IsolatedCreateOptions) => Promise<IsolatedSandboxHandle>;
98
+ }
99
+ /** Provider accepted by Shipyard's Docker execution path. */
100
+ type SandboxProvider = IsolatedSandboxProvider;
101
+ /** Create a temporary branch and merge its commits back to host HEAD. */
102
+ interface MergeToHeadBranchStrategy {
103
+ /** Branch strategy discriminator. */
104
+ readonly type: "merge-to-head";
105
+ }
106
+ /** Run on a caller-named branch. */
107
+ interface NamedBranchStrategy {
108
+ /** Branch strategy discriminator. */
109
+ readonly type: "branch";
110
+ /** Branch to create or reuse. */
111
+ readonly branch: string;
112
+ /** Starting ref for a new branch. */
113
+ readonly baseBranch?: string;
114
+ }
115
+ /** Supported Docker branch strategies. */
116
+ type BranchStrategy = MergeToHeadBranchStrategy | NamedBranchStrategy;
117
+ /** Construct the isolated provider contract used by Docker. */
118
+ declare const createIsolatedSandboxProvider: (config: IsolatedSandboxProviderConfig) => IsolatedSandboxProvider;
119
+
120
+ /**
121
+ * User-facing mount configuration for Docker.
122
+ *
123
+ * Each entry describes a host directory to mount into the sandbox container.
124
+ */
125
+ /** A single bind-mount descriptor for the Docker provider. */
126
+ interface MountConfig {
127
+ /**
128
+ * Path on the host. Supports:
129
+ * - Absolute paths (`/data/cache`)
130
+ * - Tilde-expanded paths (`~/data` → `<home>/data`)
131
+ * - Relative paths (`data` or `./data`) — resolved from `process.cwd()`
132
+ */
133
+ readonly hostPath: string;
134
+ /**
135
+ * Path inside the sandbox container. Supports:
136
+ * - Absolute paths (`/mnt/data`)
137
+ * - Tilde-expanded paths (`~/.npm` → `/home/agent/.npm`) — expanded using the provider's sandbox home directory
138
+ * - Relative paths (`data` or `./data`) — resolved from the worktree directory (`/home/agent/workspace`)
139
+ */
140
+ readonly sandboxPath: string;
141
+ /** Mount as read-only. Defaults to `false`. */
142
+ readonly readonly?: boolean;
143
+ }
144
+
145
+ export { type BranchStrategy as B, type ExecResult as E, type IsolatedSandboxProvider as I, type MountConfig as M, type NamedBranchStrategy as N, type SessionTransferHandle as S, type SandboxProvider as a, type MergeToHeadBranchStrategy as b, type InteractiveExecOptions as c, type IsolatedCreateOptions as d, type IsolatedSandboxHandle as e, type IsolatedSandboxProviderConfig as f, createIsolatedSandboxProvider as g };
@@ -1,12 +1,12 @@
1
- import { assertProtectedDirectoryIdentities, ACTIVATION_LABEL, CONFIG_DIR, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, resolveUserMounts, assertIsolatedWorkspaceMounts, processFileMountParents, defaultImageName, registerShutdown, DockerError, repositoryRunnerEnvironment, formatVolumeMount } from './chunk-FDYOTN55.js';
2
- import { createIsolatedSandboxProvider } from './chunk-KMGNFXKN.js';
3
- import { MAX_TAIL_CHARS, collectProcessOutput } from './chunk-ACD46ZM4.js';
1
+ import { RUNTIME_NAMESPACE, assertProtectedDirectoryIdentities, ACTIVATION_LABEL, CONFIG_DIR, RUNNER_DIR, createIsolatedSandboxProvider, registerShutdown, DockerError, repositoryRunnerEnvironment, SANDBOX_REPO_DIR } from './chunk-L6PX5QTU.js';
4
2
  import { execFile, execFileSync, spawn } from 'child_process';
5
- import { resolve, join, posix } from 'path';
3
+ import { resolve, join, posix, dirname, relative, isAbsolute } from 'path';
6
4
  import { promisify } from 'util';
7
5
  import { randomUUID } from 'crypto';
8
6
  import { Effect } from 'effect';
9
- import { constants } from 'fs';
7
+ import { constants, existsSync, statSync } from 'fs';
8
+ import { homedir } from 'os';
9
+ import { createInterface } from 'readline';
10
10
  import { rm, lstat, realpath, mkdir, readFile, access } from 'fs/promises';
11
11
 
12
12
  var exec = promisify(execFile);
@@ -38,6 +38,79 @@ var copyIntoContainer = async (engine, container, owner, hostPath, sandboxPath)
38
38
  );
39
39
  }
40
40
  };
41
+ var defaultImageName = (repoDir) => {
42
+ const dirName = repoDir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "local";
43
+ const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
44
+ return `${RUNTIME_NAMESPACE}:${sanitized || "local"}`;
45
+ };
46
+ var expandTilde = (p, homeDirPath) => {
47
+ const home = homeDirPath ?? homedir();
48
+ if (p === "~") return home;
49
+ if (p.startsWith("~/") || p.startsWith("~\\")) return home + "/" + p.slice(2);
50
+ return p;
51
+ };
52
+ var resolveHostPath = (hostPath) => {
53
+ const expanded = expandTilde(hostPath);
54
+ return isAbsolute(expanded) ? expanded : resolve(process.cwd(), expanded);
55
+ };
56
+ var resolveSandboxPath = (sandboxPath, sandboxHomedir) => {
57
+ const hasTilde = sandboxPath === "~" || sandboxPath.startsWith("~/") || sandboxPath.startsWith("~\\");
58
+ const expanded = hasTilde ? expandTilde(sandboxPath, sandboxHomedir) : sandboxPath;
59
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(SANDBOX_REPO_DIR, expanded);
60
+ };
61
+ var resolveUserMounts = (mounts, sandboxHomedir) => mounts.map((m) => {
62
+ const resolvedHostPath = resolveHostPath(m.hostPath);
63
+ if (!existsSync(resolvedHostPath)) {
64
+ throw new Error(
65
+ `Mount hostPath does not exist: ${m.hostPath}` + (m.hostPath !== resolvedHostPath ? ` (resolved to ${resolvedHostPath})` : "")
66
+ );
67
+ }
68
+ return {
69
+ hostPath: resolvedHostPath,
70
+ sandboxPath: resolveSandboxPath(m.sandboxPath, sandboxHomedir),
71
+ ...m.readonly ? { readonly: true } : {}
72
+ };
73
+ });
74
+ var formatVolumeMount = (mount, selinuxLabel) => {
75
+ const base = `${mount.hostPath}:${mount.sandboxPath}`;
76
+ const options = [mount.readonly ? "ro" : void 0, selinuxLabel || void 0].filter((option) => option !== void 0).join(",");
77
+ return options ? `${base}:${options}` : base;
78
+ };
79
+ var processFileMountParents = (mounts, sandboxHomedir, statFn = statSync) => {
80
+ const parentDirs = /* @__PURE__ */ new Set();
81
+ for (const mount of mounts) {
82
+ let isFile;
83
+ try {
84
+ isFile = statFn(mount.hostPath).isFile();
85
+ } catch {
86
+ continue;
87
+ }
88
+ if (!isFile) continue;
89
+ const parentDir = resolve(dirname(mount.sandboxPath));
90
+ const homeDir = resolve(sandboxHomedir);
91
+ if (parentDir === homeDir) continue;
92
+ const fromHome = relative(homeDir, parentDir);
93
+ if (fromHome === ".." || fromHome.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(fromHome)) {
94
+ throw new Error(
95
+ `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.`
96
+ );
97
+ }
98
+ parentDirs.add(parentDir);
99
+ }
100
+ return [...parentDirs];
101
+ };
102
+ var assertIsolatedWorkspaceMounts = (mounts) => {
103
+ for (const mount of mounts) {
104
+ const path = posix.resolve(mount.sandboxPath);
105
+ if (path === "/" || path === SANDBOX_REPO_DIR || path.startsWith(`${SANDBOX_REPO_DIR}/`) || SANDBOX_REPO_DIR.startsWith(`${path}/`)) {
106
+ throw new Error(
107
+ `Mount overlaps the isolated workspace: ${mount.sandboxPath}. Mount outside ${SANDBOX_REPO_DIR} or use copyToWorktree.`
108
+ );
109
+ }
110
+ }
111
+ };
112
+
113
+ // src/DockerLifecycle.ts
41
114
  var dockerExec = (args) => Effect.async((resume) => {
42
115
  execFile(
43
116
  "docker",
@@ -148,6 +221,137 @@ var removeContainer = (containerName) => Effect.gen(function* () {
148
221
  var removeImage = (imageName) => Effect.gen(function* () {
149
222
  yield* dockerExec(["rmi", imageName]);
150
223
  });
224
+
225
+ // src/boundedTail.ts
226
+ var MAX_TAIL_CHARS = 64 * 1024;
227
+ var OutputByteCounter = class {
228
+ constructor(maxBytes) {
229
+ this.maxBytes = maxBytes;
230
+ }
231
+ totalBytes = 0;
232
+ limitExceeded = false;
233
+ add(chunk) {
234
+ this.totalBytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
235
+ if (this.totalBytes > this.maxBytes) this.limitExceeded = true;
236
+ }
237
+ get exceeded() {
238
+ return this.limitExceeded;
239
+ }
240
+ };
241
+ var BoundedTail = class {
242
+ items = [];
243
+ totalChars = 0;
244
+ maxChars;
245
+ separator;
246
+ /**
247
+ * @param maxChars Maximum length of the joined tail. Defaults to {@link MAX_TAIL_CHARS}.
248
+ * @param separator String placed between items by {@link toString}. Must match
249
+ * how the caller would otherwise have joined the accumulated chunks (e.g.
250
+ * `"\n"` for line streams, `""` for raw chunk streams).
251
+ */
252
+ constructor(maxChars = MAX_TAIL_CHARS, separator = "") {
253
+ this.maxChars = maxChars;
254
+ this.separator = separator;
255
+ }
256
+ /** Append one item to the tail, evicting oldest items to stay within budget. */
257
+ push(item) {
258
+ const bounded = item.length > this.maxChars ? item.slice(item.length - this.maxChars) : item;
259
+ this.totalChars += bounded.length + (this.items.length > 0 ? this.separator.length : 0);
260
+ this.items.push(bounded);
261
+ while (this.totalChars > this.maxChars && this.items.length > 1) {
262
+ const dropped = this.items.shift();
263
+ this.totalChars -= dropped.length + this.separator.length;
264
+ }
265
+ }
266
+ /** Join the retained tail into a single string (length ≤ `maxChars`). */
267
+ toString() {
268
+ return this.items.join(this.separator);
269
+ }
270
+ };
271
+ var StreamedProcessOutput = class {
272
+ maxOutputBytes;
273
+ byteCounter;
274
+ stdoutTail;
275
+ stderrTail;
276
+ onLine;
277
+ outputLimitError;
278
+ constructor(options) {
279
+ this.maxOutputBytes = options.maxOutputBytes;
280
+ this.byteCounter = options.maxOutputBytes === void 0 ? void 0 : new OutputByteCounter(options.maxOutputBytes);
281
+ const tailChars = options.maxOutputBytes ?? options.maxOutputTailChars ?? MAX_TAIL_CHARS;
282
+ this.stdoutTail = new BoundedTail(tailChars, "\n");
283
+ this.stderrTail = new BoundedTail(tailChars, "");
284
+ this.onLine = options.onLine ?? (() => {
285
+ });
286
+ }
287
+ checkLimit(chunk) {
288
+ if (this.byteCounter === void 0 || this.outputLimitError !== void 0) {
289
+ return this.outputLimitError;
290
+ }
291
+ this.byteCounter.add(chunk);
292
+ if (this.byteCounter.exceeded) {
293
+ this.outputLimitError = new Error(
294
+ `Sandbox command output exceeded ${this.maxOutputBytes} bytes`
295
+ );
296
+ }
297
+ return this.outputLimitError;
298
+ }
299
+ addStdoutLine(line) {
300
+ this.stdoutTail.push(line);
301
+ this.onLine(line);
302
+ }
303
+ addStderr(chunk) {
304
+ this.stderrTail.push(chunk);
305
+ }
306
+ get error() {
307
+ return this.outputLimitError;
308
+ }
309
+ result(exitCode) {
310
+ return {
311
+ stdout: this.stdoutTail.toString(),
312
+ stderr: this.stderrTail.toString(),
313
+ exitCode
314
+ };
315
+ }
316
+ };
317
+ var collectProcessOutput = (process2, options, resolve3, reject) => {
318
+ if (options.onLine || options.maxOutputBytes !== void 0) {
319
+ const output = new StreamedProcessOutput(options);
320
+ const checkLimit = (chunk) => {
321
+ if (output.checkLimit(chunk) !== void 0) process2.kill();
322
+ };
323
+ process2.stdout.on("data", checkLimit);
324
+ process2.stderr.on("data", checkLimit);
325
+ const lines = createInterface({ input: process2.stdout });
326
+ lines.on("line", (line) => output.addStdoutLine(line));
327
+ process2.stderr.on("data", (chunk) => {
328
+ output.addStderr(chunk.toString());
329
+ });
330
+ process2.onClose((code) => {
331
+ if (output.error !== void 0) {
332
+ reject(output.error);
333
+ return;
334
+ }
335
+ resolve3(output.result(code ?? 0));
336
+ });
337
+ return;
338
+ }
339
+ const stdoutChunks = [];
340
+ const stderrChunks = [];
341
+ process2.stdout.on("data", (chunk) => {
342
+ stdoutChunks.push(chunk.toString());
343
+ });
344
+ process2.stderr.on("data", (chunk) => {
345
+ stderrChunks.push(chunk.toString());
346
+ });
347
+ process2.onClose((code) => {
348
+ resolve3({
349
+ stdout: stdoutChunks.join(""),
350
+ stderr: stderrChunks.join(""),
351
+ exitCode: code ?? 0
352
+ });
353
+ });
354
+ };
151
355
  var execFileAsync = promisify(execFile);
152
356
  var RUNNER_INSTALL_METADATA = ".shipyard-install.json";
153
357
  var RUNNER_CONTROLLER_LOCK = ".shipyard-controller.lock";
@@ -216,7 +420,7 @@ var defaultAdapters = {
216
420
  }
217
421
  },
218
422
  signalProcess: (pid, signal) => process.kill(pid, signal),
219
- pause: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
423
+ pause: (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds))
220
424
  };
221
425
  var lockOwnsProcess = async (lock, adapters) => lock !== void 0 && Number.isInteger(lock.pid) && typeof lock.processStartedAt === "string" && lock.processStartedAt.length > 0 && adapters.isProcessRunning(lock.pid) && await adapters.processIdentity(lock.pid) === lock.processStartedAt;
222
426
  var lifecycleFailure = (purpose, error) => error instanceof RunnerLifecycleError ? error : new RunnerLifecycleError(
@@ -274,12 +478,8 @@ var listRemoteRunners = async (repository, repoDir, environment, adapters) => {
274
478
  return parseRemoteRunners(response.stdout);
275
479
  };
276
480
  var validateExistingRepositoryRunner = async (options, adapters = defaultAdapters) => {
277
- if (!await adapters.exists(options.maskDir)) {
278
- await adapters.makeDirectory(options.maskDir);
279
- }
280
481
  await assertProtectedDirectoryIdentities(
281
482
  options.runnerDir,
282
- options.maskDir,
283
483
  adapters.inspectDirectory
284
484
  ).catch((error) => {
285
485
  throw lifecycleFailure("Validating protected runner directories", error);
@@ -379,12 +579,8 @@ var recoverRepositoryRunner = async (options, adapters = defaultAdapters) => {
379
579
  if (lock) await adapters.remove(lockPath);
380
580
  const workDir = join(options.runnerDir, RUNNER_WORK_DIR);
381
581
  if (await adapters.exists(workDir)) await adapters.remove(workDir);
382
- if (await adapters.exists(options.maskDir))
383
- await adapters.remove(options.maskDir);
384
- await adapters.makeDirectory(options.maskDir);
385
582
  await assertProtectedDirectoryIdentities(
386
583
  options.runnerDir,
387
- options.maskDir,
388
584
  adapters.inspectDirectory
389
585
  ).catch((error) => {
390
586
  throw lifecycleFailure("Validating protected runner directories", error);
@@ -536,7 +732,6 @@ var unregisterRepositoryRunner = async (metadata, repoDir, runnerDir, environmen
536
732
  var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
537
733
  const configDir = join(options.repoDir, CONFIG_DIR);
538
734
  const runnerDir = join(configDir, RUNNER_DIR);
539
- const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
540
735
  if (!await adapters.exists(runnerDir)) {
541
736
  throw new RunnerLifecycleError(
542
737
  `No repository runner is installed at ${runnerDir}.`
@@ -548,7 +743,6 @@ var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
548
743
  } catch (error) {
549
744
  if (!options.force) throw error;
550
745
  await adapters.remove(runnerDir);
551
- if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
552
746
  return {
553
747
  removed: true,
554
748
  forced: true,
@@ -578,14 +772,12 @@ var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
578
772
  );
579
773
  }
580
774
  await adapters.remove(runnerDir);
581
- if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
582
775
  return {
583
776
  removed: true,
584
777
  forced: true,
585
778
  manualCleanup: `Remove runner ${metadata.name} from ${metadata.repository} in GitHub Settings > Actions > Runners.`
586
779
  };
587
780
  }
588
- if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
589
781
  await adapters.remove(runnerDir);
590
782
  return { removed: true, forced: false };
591
783
  };
@@ -642,7 +834,7 @@ var docker = (options) => {
642
834
  )
643
835
  );
644
836
  for (const dir of parentDirsToCreate) {
645
- await new Promise((resolve2, reject) => {
837
+ await new Promise((resolve3, reject) => {
646
838
  execFile(
647
839
  "docker",
648
840
  [
@@ -665,7 +857,7 @@ var docker = (options) => {
665
857
  )
666
858
  );
667
859
  } else {
668
- resolve2();
860
+ resolve3();
669
861
  }
670
862
  }
671
863
  );
@@ -688,7 +880,7 @@ var docker = (options) => {
688
880
  if (opts?.stdin !== void 0) args.push("-i");
689
881
  if (opts?.cwd) args.push("-w", opts.cwd);
690
882
  args.push(containerName, "sh", "-c", effectiveCommand);
691
- return new Promise((resolve2, reject) => {
883
+ return new Promise((resolve3, reject) => {
692
884
  const proc = spawn("docker", args, {
693
885
  signal: opts?.signal,
694
886
  stdio: [
@@ -712,13 +904,13 @@ var docker = (options) => {
712
904
  onClose: (listener) => proc.on("close", listener)
713
905
  },
714
906
  { ...opts, maxOutputTailChars },
715
- resolve2,
907
+ resolve3,
716
908
  reject
717
909
  );
718
910
  });
719
911
  },
720
912
  interactiveExec: (args, opts) => {
721
- return new Promise((resolve2, reject) => {
913
+ return new Promise((resolve3, reject) => {
722
914
  opts.signal?.throwIfAborted();
723
915
  const executionId = randomUUID();
724
916
  const pidFile = `/tmp/shipyard-interactive-${executionId}.pid`;
@@ -750,7 +942,7 @@ var docker = (options) => {
750
942
  settled = true;
751
943
  opts.signal?.removeEventListener("abort", onAbort);
752
944
  if ("error" in result) reject(result.error);
753
- else resolve2(result);
945
+ else resolve3(result);
754
946
  };
755
947
  const onAbort = () => {
756
948
  if (cancelling || settled) return;
@@ -813,7 +1005,7 @@ var docker = (options) => {
813
1005
  hostPath,
814
1006
  sandboxPath
815
1007
  ),
816
- copyFileOut: (sandboxPath, hostPath) => new Promise((resolve2, reject) => {
1008
+ copyFileOut: (sandboxPath, hostPath) => new Promise((resolve3, reject) => {
817
1009
  execFile(
818
1010
  "docker",
819
1011
  ["cp", `${containerName}:${sandboxPath}`, hostPath],
@@ -821,7 +1013,7 @@ var docker = (options) => {
821
1013
  if (error) {
822
1014
  reject(new Error(`docker cp (out) failed: ${error.message}`));
823
1015
  } else {
824
- resolve2();
1016
+ resolve3();
825
1017
  }
826
1018
  }
827
1019
  );
@@ -835,7 +1027,7 @@ var docker = (options) => {
835
1027
  }
836
1028
  });
837
1029
  };
838
- var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) => {
1030
+ var checkImageUid = (imageName, expectedUid) => new Promise((resolve3, reject) => {
839
1031
  execFile(
840
1032
  "docker",
841
1033
  ["image", "inspect", imageName, "--format", "{{.Config.User}}"],
@@ -850,13 +1042,13 @@ var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) =
850
1042
  }
851
1043
  const imageUser = (stdout ?? "").toString().trim();
852
1044
  if (!imageUser) {
853
- resolve2();
1045
+ resolve3();
854
1046
  return;
855
1047
  }
856
1048
  const uidPart = imageUser.split(":")[0];
857
1049
  const imageUid = parseInt(uidPart, 10);
858
1050
  if (isNaN(imageUid)) {
859
- resolve2();
1051
+ resolve3();
860
1052
  return;
861
1053
  }
862
1054
  if (imageUid !== expectedUid) {
@@ -866,12 +1058,12 @@ var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) =
866
1058
  )
867
1059
  );
868
1060
  } else {
869
- resolve2();
1061
+ resolve3();
870
1062
  }
871
1063
  }
872
1064
  );
873
1065
  });
874
1066
 
875
- export { REPOSITORY_RUNNER_OWNER_ENV, RUNNER_CONTROLLER_LOCK, RUNNER_INSTALL_METADATA, RUNNER_WORK_DIR, RunnerLifecycleError, buildImage, docker, recoverRepositoryRunner, removeImage, removeOwnedRepositoryRunnerContainers, removeRepositoryRunner, validateExistingRepositoryRunner };
876
- //# sourceMappingURL=chunk-WAXEMZUV.js.map
877
- //# sourceMappingURL=chunk-WAXEMZUV.js.map
1067
+ export { REPOSITORY_RUNNER_OWNER_ENV, RUNNER_CONTROLLER_LOCK, RUNNER_INSTALL_METADATA, RUNNER_WORK_DIR, RunnerLifecycleError, buildImage, defaultImageName, docker, recoverRepositoryRunner, removeImage, removeOwnedRepositoryRunnerContainers, removeRepositoryRunner, validateExistingRepositoryRunner };
1068
+ //# sourceMappingURL=chunk-44I2BL6E.js.map
1069
+ //# sourceMappingURL=chunk-44I2BL6E.js.map