@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.
- package/README.md +34 -41
- package/dist/MountConfig-BHnKnA4h.d.ts +145 -0
- package/dist/{chunk-WAXEMZUV.js → chunk-44I2BL6E.js} +225 -33
- package/dist/chunk-44I2BL6E.js.map +1 -0
- package/dist/{chunk-56TDSWFU.js → chunk-JI3HDDMS.js} +3 -3
- package/dist/{chunk-56TDSWFU.js.map → chunk-JI3HDDMS.js.map} +1 -1
- package/dist/{chunk-FDYOTN55.js → chunk-L6PX5QTU.js} +253 -878
- package/dist/chunk-L6PX5QTU.js.map +1 -0
- package/dist/index.d.ts +117 -97
- package/dist/index.js +322 -344
- package/dist/index.js.map +1 -1
- package/dist/main.js +235 -139
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.d.ts +1 -3
- package/dist/sandboxes/docker.js +2 -4
- package/dist/templates/parallel-planner/main.mts +67 -9
- package/dist/templates/parallel-planner/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner-with-review/main.mts +70 -12
- package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
- package/dist/templates/sequential-reviewer/main.mts +52 -3
- package/dist/templates/simple-loop/main.mts +51 -2
- package/package.json +1 -17
- package/dist/MountConfig-bZoCs4Dd.d.ts +0 -26
- package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
- package/dist/chunk-ACD46ZM4.js +0 -136
- package/dist/chunk-ACD46ZM4.js.map +0 -1
- package/dist/chunk-FDYOTN55.js.map +0 -1
- package/dist/chunk-KMGNFXKN.js +0 -38
- package/dist/chunk-KMGNFXKN.js.map +0 -1
- package/dist/chunk-SOJTAJTF.js +0 -78
- package/dist/chunk-SOJTAJTF.js.map +0 -1
- package/dist/chunk-WAXEMZUV.js.map +0 -1
- package/dist/sandboxes/no-sandbox.d.ts +0 -37
- package/dist/sandboxes/no-sandbox.js +0 -4
- package/dist/sandboxes/no-sandbox.js.map +0 -1
- package/dist/sandboxes/vercel.d.ts +0 -104
- package/dist/sandboxes/vercel.js +0 -166
- package/dist/sandboxes/vercel.js.map +0 -1
- package/dist/templates/blank/main.mts +0 -13
- package/dist/templates/blank/prompt.md +0 -12
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
```sh
|
|
38
|
+
npm install --save-dev @snappedly-tools/shipyard
|
|
39
|
+
npx shipyard init
|
|
40
|
+
```
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
| `
|
|
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,
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
907
|
+
resolve3,
|
|
716
908
|
reject
|
|
717
909
|
);
|
|
718
910
|
});
|
|
719
911
|
},
|
|
720
912
|
interactiveExec: (args, opts) => {
|
|
721
|
-
return new Promise((
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
877
|
-
//# sourceMappingURL=chunk-
|
|
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
|