@snappedly-tools/shipyard 0.5.0-staging.36072068333 → 0.6.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 +165 -64
- package/dist/MountConfig-bZoCs4Dd.d.ts +26 -0
- package/dist/SandboxProvider-XJQqEdSf.d.ts +261 -0
- package/dist/{chunk-JI3HDDMS.js → chunk-56TDSWFU.js} +3 -3
- package/dist/{chunk-JI3HDDMS.js.map → chunk-56TDSWFU.js.map} +1 -1
- package/dist/chunk-ACD46ZM4.js +136 -0
- package/dist/chunk-ACD46ZM4.js.map +1 -0
- package/dist/{chunk-L6PX5QTU.js → chunk-FDYOTN55.js} +887 -262
- package/dist/chunk-FDYOTN55.js.map +1 -0
- package/dist/chunk-KMGNFXKN.js +38 -0
- package/dist/chunk-KMGNFXKN.js.map +1 -0
- package/dist/chunk-SOJTAJTF.js +78 -0
- package/dist/chunk-SOJTAJTF.js.map +1 -0
- package/dist/{chunk-44I2BL6E.js → chunk-WAXEMZUV.js} +33 -225
- package/dist/chunk-WAXEMZUV.js.map +1 -0
- package/dist/index.d.ts +97 -117
- package/dist/index.js +344 -322
- package/dist/index.js.map +1 -1
- package/dist/main.js +139 -235
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.d.ts +3 -1
- package/dist/sandboxes/docker.js +4 -2
- package/dist/sandboxes/no-sandbox.d.ts +37 -0
- package/dist/sandboxes/no-sandbox.js +4 -0
- package/dist/sandboxes/no-sandbox.js.map +1 -0
- package/dist/sandboxes/vercel.d.ts +104 -0
- package/dist/sandboxes/vercel.js +166 -0
- package/dist/sandboxes/vercel.js.map +1 -0
- package/dist/templates/blank/main.mts +13 -0
- package/dist/templates/blank/prompt.md +12 -0
- package/dist/templates/blank/template.json +4 -0
- package/dist/templates/parallel-planner/main.mts +9 -67
- package/dist/templates/parallel-planner-with-review/main.mts +12 -70
- package/dist/templates/sequential-reviewer/main.mts +3 -52
- package/dist/templates/simple-loop/main.mts +2 -51
- package/package.json +17 -1
- package/dist/MountConfig-BHnKnA4h.d.ts +0 -145
- package/dist/chunk-44I2BL6E.js.map +0 -1
- package/dist/chunk-L6PX5QTU.js.map +0 -1
- package/dist/templates/parallel-planner/planner-branch.mts +0 -151
- package/dist/templates/parallel-planner-with-review/planner-branch.mts +0 -151
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/SandboxProvider.ts
|
|
2
|
+
var createBindMountSandboxProvider = (config) => ({
|
|
3
|
+
tag: "bind-mount",
|
|
4
|
+
name: config.name,
|
|
5
|
+
env: config.env ?? {},
|
|
6
|
+
sandboxHomedir: config.sandboxHomedir,
|
|
7
|
+
create: config.create
|
|
8
|
+
});
|
|
9
|
+
var createIsolatedSandboxProvider = (config) => ({
|
|
10
|
+
tag: "isolated",
|
|
11
|
+
name: config.name,
|
|
12
|
+
env: config.env ?? {},
|
|
13
|
+
create: config.create
|
|
14
|
+
});
|
|
15
|
+
var toSessionTransferHandle = (handle) => {
|
|
16
|
+
if (handle === void 0 || !("copyFileOut" in handle)) return void 0;
|
|
17
|
+
if ("copyFileIn" in handle) return handle;
|
|
18
|
+
return {
|
|
19
|
+
worktreePath: handle.worktreePath,
|
|
20
|
+
exec: (command, options) => handle.exec(command, options),
|
|
21
|
+
interactiveExec: handle.interactiveExec?.bind(handle),
|
|
22
|
+
copyFileIn: (source, destination) => handle.copyIn(source, destination),
|
|
23
|
+
copyFileOut: (source, destination) => handle.copyFileOut(source, destination),
|
|
24
|
+
close: () => handle.close()
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/shellQuote.ts
|
|
29
|
+
var shellQuote = (value) => {
|
|
30
|
+
if (value.includes("\0")) {
|
|
31
|
+
throw new Error("Cannot quote a string containing a NUL byte");
|
|
32
|
+
}
|
|
33
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { createBindMountSandboxProvider, createIsolatedSandboxProvider, shellQuote, toSessionTransferHandle };
|
|
37
|
+
//# sourceMappingURL=chunk-KMGNFXKN.js.map
|
|
38
|
+
//# sourceMappingURL=chunk-KMGNFXKN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/SandboxProvider.ts","../src/shellQuote.ts"],"names":[],"mappings":";AAsUO,IAAM,8BAAA,GAAiC,CAC5C,MAAA,MAC8B;AAAA,EAC9B,GAAA,EAAK,YAAA;AAAA,EACL,MAAM,MAAA,CAAO,IAAA;AAAA,EACb,GAAA,EAAK,MAAA,CAAO,GAAA,IAAO,EAAC;AAAA,EACpB,gBAAgB,MAAA,CAAO,cAAA;AAAA,EACvB,QAAQ,MAAA,CAAO;AACjB,CAAA;AAMO,IAAM,6BAAA,GAAgC,CAC3C,MAAA,MAC6B;AAAA,EAC7B,GAAA,EAAK,UAAA;AAAA,EACL,MAAM,MAAA,CAAO,IAAA;AAAA,EACb,GAAA,EAAK,MAAA,CAAO,GAAA,IAAO,EAAC;AAAA,EACpB,QAAQ,MAAA,CAAO;AACjB,CAAA;AAGO,IAAM,uBAAA,GAA0B,CACrC,MAAA,KAKuC;AACvC,EAAA,IAAI,MAAA,KAAW,MAAA,IAAa,EAAE,aAAA,IAAiB,SAAS,OAAO,MAAA;AAC/D,EAAA,IAAI,YAAA,IAAgB,QAAQ,OAAO,MAAA;AACnC,EAAA,OAAO;AAAA,IACL,cAAc,MAAA,CAAO,YAAA;AAAA,IACrB,MAAM,CAAC,OAAA,EAAS,YAAY,MAAA,CAAO,IAAA,CAAK,SAAS,OAAO,CAAA;AAAA,IACxD,eAAA,EAAiB,MAAA,CAAO,eAAA,EAAiB,IAAA,CAAK,MAAM,CAAA;AAAA,IACpD,YAAY,CAAC,MAAA,EAAQ,gBAAgB,MAAA,CAAO,MAAA,CAAO,QAAQ,WAAW,CAAA;AAAA,IACtE,aAAa,CAAC,MAAA,EAAQ,gBACpB,MAAA,CAAO,WAAA,CAAY,QAAQ,WAAW,CAAA;AAAA,IACxC,KAAA,EAAO,MAAM,MAAA,CAAO,KAAA;AAAM,GAC5B;AACF;;;AC/WO,IAAM,UAAA,GAAa,CAAC,KAAA,KAA0B;AACnD,EAAA,IAAI,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,EAC/D;AACA,EAAA,OAAO,CAAA,CAAA,EAAI,KAAA,CAAM,UAAA,CAAW,GAAA,EAAK,OAAO,CAAC,CAAA,CAAA,CAAA;AAC3C","file":"chunk-KMGNFXKN.js","sourcesContent":["/**\n * Sandbox provider types — the pluggable interface for sandbox runtimes.\n *\n * Provider authors implement a small Promise-based interface. Shipyard\n * handles worktree creation, git mount resolution, and commit extraction.\n */\n\n/** Result of executing a command inside a sandbox. */\nexport interface ExecResult {\n readonly stdout: string;\n readonly stderr: string;\n readonly exitCode: number;\n}\n\n/** Options for interactiveExec — the streams the provider should wire to the spawned process. */\nexport interface InteractiveExecOptions {\n readonly stdin: NodeJS.ReadableStream;\n readonly stdout: NodeJS.WritableStream;\n readonly stderr: NodeJS.WritableStream;\n readonly cwd?: string;\n /** Terminate the interactive process when the caller cancels the session. */\n readonly signal?: AbortSignal;\n}\n\n/** Handle to a running bind-mount sandbox. */\nexport interface BindMountSandboxHandle {\n /** Absolute path to the worktree inside the sandbox. */\n readonly worktreePath: string;\n /**\n * Execute a command in the sandbox.\n *\n * Implementations MUST support line-by-line streaming via `onLine`. This is\n * how Shipyard delivers live feedback to the user and enforces idle timeouts —\n * without a streaming implementation, neither will work. A buffered/batch\n * implementation that only calls `onLine` after the process exits does NOT\n * satisfy this contract.\n *\n * When `stdin` is set, the implementation pipes the string to the child\n * process's stdin and closes it. This avoids the Linux 128 KB per-arg limit.\n */\n exec(\n command: string,\n options?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n /** Abort the command when the caller's operation is cancelled. */\n signal?: AbortSignal;\n /** Reject/terminate the command after this many combined output bytes. */\n maxOutputBytes?: number;\n },\n ): Promise<ExecResult>;\n /**\n * Launch an interactive process inside the sandbox.\n * Optional — providers that support interactive sessions implement this.\n * The provider detects TTY mode from the streams (e.g. stdin.isTTY) and\n * allocates a pseudo-terminal accordingly.\n * Implementations MUST terminate the underlying process when `signal` aborts.\n */\n interactiveExec?(\n args: string[],\n options: InteractiveExecOptions,\n ): Promise<{ exitCode: number }>;\n /** Copy a single file from the host into the sandbox. */\n copyFileIn(hostPath: string, sandboxPath: string): Promise<void>;\n /** Copy a single file from the sandbox to the host. */\n copyFileOut(sandboxPath: string, hostPath: string): Promise<void>;\n /** Tear down the sandbox. */\n close(): Promise<void>;\n}\n\n/** Options passed to a bind-mount provider's `create` function. */\nexport interface BindMountCreateOptions {\n /** Host-side path to the worktree directory. */\n readonly worktreePath: string;\n /** Host-side path to the original repo root. */\n readonly hostRepoPath: string;\n /** Volume mounts to apply (host:sandbox pairs). */\n readonly mounts: Array<{\n hostPath: string;\n sandboxPath: string;\n readonly?: boolean;\n }>;\n /** Environment variables to inject into the sandbox. */\n readonly env: Record<string, string>;\n}\n\n/** Configuration for createBindMountSandboxProvider. */\nexport interface BindMountSandboxProviderConfig {\n /** Human-readable name for this provider (e.g. \"docker\"). */\n readonly name: string;\n /** Environment variables injected by this provider. Merged at launch time. */\n readonly env?: Record<string, string>;\n /**\n * Absolute path to the home directory inside the sandbox (e.g. `\"/home/agent\"`).\n * Used to expand `~` in user-provided `sandboxPath` mount configs.\n * Set to `undefined` for providers that do not have a fixed home directory.\n */\n readonly sandboxHomedir?: string;\n /** Create a sandbox handle from the given options. */\n readonly create: (\n options: BindMountCreateOptions,\n ) => Promise<BindMountSandboxHandle>;\n}\n\n/** Handle to a running isolated sandbox (extends bind-mount with file transfer). */\nexport interface IsolatedSandboxHandle {\n /** Absolute path to the worktree inside the sandbox. */\n readonly worktreePath: string;\n /**\n * Execute a command in the sandbox.\n *\n * Implementations MUST support line-by-line streaming via `onLine`. This is\n * how Shipyard delivers live feedback to the user and enforces idle timeouts —\n * without a streaming implementation, neither will work. A buffered/batch\n * implementation that only calls `onLine` after the process exits does NOT\n * satisfy this contract.\n *\n * When `stdin` is set, the implementation pipes the string to the child\n * process's stdin and closes it. This avoids the Linux 128 KB per-arg limit.\n */\n exec(\n command: string,\n options?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n /** Abort the command when the caller's operation is cancelled. */\n signal?: AbortSignal;\n /** Reject/terminate the command after this many combined output bytes. */\n maxOutputBytes?: number;\n },\n ): Promise<ExecResult>;\n /**\n * Launch an interactive process inside the sandbox.\n * Optional — providers that support interactive sessions implement this.\n * The provider detects TTY mode from the streams (e.g. stdin.isTTY) and\n * allocates a pseudo-terminal accordingly.\n * Implementations MUST terminate the underlying process when `signal` aborts.\n */\n interactiveExec?(\n args: string[],\n options: InteractiveExecOptions,\n ): Promise<{ exitCode: number }>;\n /** Copy a file or directory from the host into the sandbox. */\n copyIn(hostPath: string, sandboxPath: string): Promise<void>;\n /** Copy a single file from the sandbox to the host. */\n copyFileOut(sandboxPath: string, hostPath: string): Promise<void>;\n /** Tear down the sandbox. */\n close(): Promise<void>;\n}\n\n/** Options passed to an isolated provider's `create` function. */\nexport interface IsolatedCreateOptions {\n /** Original host repository path, used only for image naming; never mounted. */\n readonly hostRepoPath?: string;\n /** Environment variables to inject into the sandbox. */\n readonly env: Record<string, string>;\n}\n\n/** Configuration for createIsolatedSandboxProvider. */\nexport interface IsolatedSandboxProviderConfig {\n /** Human-readable name for this provider (e.g. \"vercel\"). */\n readonly name: string;\n /** Environment variables injected by this provider. Merged at launch time. */\n readonly env?: Record<string, string>;\n /** Create an isolated sandbox handle from the given options. */\n readonly create: (\n options: IsolatedCreateOptions,\n ) => Promise<IsolatedSandboxHandle>;\n}\n\n/** A bind-mount sandbox provider. */\nexport interface BindMountSandboxProvider {\n /** @internal Discriminator for internal dispatch. */\n readonly tag: \"bind-mount\";\n /** Human-readable provider name. */\n readonly name: string;\n /** Environment variables injected by this provider. */\n readonly env: Record<string, string>;\n /**\n * Absolute path to the home directory inside the sandbox (e.g. `\"/home/agent\"`).\n * `undefined` when the provider does not declare a sandbox home directory.\n */\n readonly sandboxHomedir: string | undefined;\n /** @internal Create a sandbox handle. */\n readonly create: (\n options: BindMountCreateOptions,\n ) => Promise<BindMountSandboxHandle>;\n}\n\n/** An isolated sandbox provider. */\nexport interface IsolatedSandboxProvider {\n /** @internal Discriminator for internal dispatch. */\n readonly tag: \"isolated\";\n /** Human-readable provider name. */\n readonly name: string;\n /** Environment variables injected by this provider. */\n readonly env: Record<string, string>;\n /** @internal Create an isolated sandbox handle. */\n readonly create: (\n options: IsolatedCreateOptions,\n ) => Promise<IsolatedSandboxHandle>;\n}\n\n/** Handle to a no-sandbox session — runs commands directly on the host. */\nexport interface NoSandboxHandle {\n /** Absolute path to the worktree on the host. */\n readonly worktreePath: string;\n /**\n * Execute a command on the host.\n *\n * Implementations MUST support line-by-line streaming via `onLine`. This is\n * how Shipyard delivers live feedback to the user and enforces idle timeouts —\n * without a streaming implementation, neither will work.\n *\n * When `stdin` is set, the implementation pipes the string to the child\n * process's stdin and closes it. This avoids the Linux 128 KB per-arg limit.\n */\n exec(\n command: string,\n options?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n /** Abort the command when the caller's operation is cancelled. */\n signal?: AbortSignal;\n /** Reject/terminate the command after this many combined output bytes. */\n maxOutputBytes?: number;\n },\n ): Promise<ExecResult>;\n /**\n * Launch an interactive process on the host with inherited stdio.\n */\n interactiveExec(\n args: string[],\n options: InteractiveExecOptions,\n ): Promise<{ exitCode: number }>;\n /** No-op — no container to tear down. */\n close(): Promise<void>;\n}\n\n/** A no-sandbox provider — runs the agent directly on the host with no container isolation. */\nexport interface NoSandboxProvider {\n /** @internal Discriminator for internal dispatch. */\n readonly tag: \"none\";\n /** Human-readable provider name. */\n readonly name: string;\n /** Environment variables injected by this provider. */\n readonly env: Record<string, string>;\n /** @internal Create a no-sandbox handle. */\n readonly create: (options: {\n readonly worktreePath: string;\n readonly env: Record<string, string>;\n }) => Promise<NoSandboxHandle>;\n}\n\n// ---------- Branch strategy types ----------\n\n/** Head strategy: agent writes directly to host working directory. Bind-mount only. */\nexport interface HeadBranchStrategy {\n readonly type: \"head\";\n}\n\n/** Merge-to-head strategy: temp branch, merge back to HEAD, delete temp branch. */\nexport interface MergeToHeadBranchStrategy {\n readonly type: \"merge-to-head\";\n}\n\n/** Branch strategy: commits land on an explicit named branch. */\nexport interface NamedBranchStrategy {\n readonly type: \"branch\";\n readonly branch: string;\n /**\n * Git ref to use as the starting point when creating a new branch.\n * Only used when the branch doesn't already exist — ignored otherwise.\n * Callers are responsible for ensuring the ref is current (e.g. `git fetch`).\n * Defaults to `HEAD` when omitted.\n */\n readonly baseBranch?: string;\n}\n\n/** Branch strategy for bind-mount providers (all three variants). */\nexport type BindMountBranchStrategy =\n | HeadBranchStrategy\n | MergeToHeadBranchStrategy\n | NamedBranchStrategy;\n\n/** Branch strategy for isolated providers (no head — can't write to host). */\nexport type IsolatedBranchStrategy =\n | MergeToHeadBranchStrategy\n | NamedBranchStrategy;\n\n/** Branch strategy for no-sandbox providers (all three — same as bind-mount). */\nexport type NoSandboxBranchStrategy =\n | HeadBranchStrategy\n | MergeToHeadBranchStrategy\n | NamedBranchStrategy;\n\n/** Union of all branch strategy variants. */\nexport type BranchStrategy =\n | BindMountBranchStrategy\n | IsolatedBranchStrategy\n | NoSandboxBranchStrategy;\n\n/**\n * A sandbox provider — the pluggable unit that `run()`, `interactive()`, and\n * `createSandbox()` accept. Tagged for internal dispatch: \"bind-mount\",\n * \"isolated\", or \"none\". When `NoSandboxProvider` is used, the agent runs\n * directly on the host with no container isolation — opt in at your own risk.\n */\nexport type SandboxProvider =\n | BindMountSandboxProvider\n | IsolatedSandboxProvider\n | NoSandboxProvider;\n\n/** @deprecated Use `SandboxProvider` — it now includes `NoSandboxProvider`. */\nexport type AnySandboxProvider = SandboxProvider;\n\n/**\n * Create a bind-mount sandbox provider from a config object.\n * The returned provider can be passed to `run()` or `createSandbox()`.\n */\nexport const createBindMountSandboxProvider = (\n config: BindMountSandboxProviderConfig,\n): BindMountSandboxProvider => ({\n tag: \"bind-mount\",\n name: config.name,\n env: config.env ?? {},\n sandboxHomedir: config.sandboxHomedir,\n create: config.create,\n});\n\n/**\n * Create an isolated sandbox provider from a config object.\n * The returned provider can be passed to `run()` or `createSandbox()`.\n */\nexport const createIsolatedSandboxProvider = (\n config: IsolatedSandboxProviderConfig,\n): IsolatedSandboxProvider => ({\n tag: \"isolated\",\n name: config.name,\n env: config.env ?? {},\n create: config.create,\n});\n\n/** @internal Adapt isolated file transfer to the existing agent-session boundary. */\nexport const toSessionTransferHandle = (\n handle:\n | BindMountSandboxHandle\n | IsolatedSandboxHandle\n | NoSandboxHandle\n | undefined,\n): BindMountSandboxHandle | undefined => {\n if (handle === undefined || !(\"copyFileOut\" in handle)) return undefined;\n if (\"copyFileIn\" in handle) return handle;\n return {\n worktreePath: handle.worktreePath,\n exec: (command, options) => handle.exec(command, options),\n interactiveExec: handle.interactiveExec?.bind(handle),\n copyFileIn: (source, destination) => handle.copyIn(source, destination),\n copyFileOut: (source, destination) =>\n handle.copyFileOut(source, destination),\n close: () => handle.close(),\n };\n};\n","/** Quote a value for use as one POSIX-shell argument. */\nexport const shellQuote = (value: string): string => {\n if (value.includes(\"\\0\")) {\n throw new Error(\"Cannot quote a string containing a NUL byte\");\n }\n return `'${value.replaceAll(\"'\", \"'\\\\''\")}'`;\n};\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MAX_TAIL_CHARS, collectProcessOutput } from './chunk-ACD46ZM4.js';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
var noSandbox = (options) => ({
|
|
5
|
+
tag: "none",
|
|
6
|
+
name: "no-sandbox",
|
|
7
|
+
env: options?.env ?? {},
|
|
8
|
+
create: async (createOptions) => {
|
|
9
|
+
const worktreePath = createOptions.worktreePath;
|
|
10
|
+
const processEnv = { ...process.env, ...createOptions.env };
|
|
11
|
+
const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
|
|
12
|
+
const handle = {
|
|
13
|
+
worktreePath,
|
|
14
|
+
exec: (command, opts) => {
|
|
15
|
+
const cwd = opts?.cwd ?? worktreePath;
|
|
16
|
+
const isWindows = process.platform === "win32";
|
|
17
|
+
const shellCmd = isWindows ? "cmd.exe" : "sh";
|
|
18
|
+
const shellArgs = isWindows ? ["/d", "/s", "/c", command] : ["-c", command];
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const proc = spawn(shellCmd, shellArgs, {
|
|
21
|
+
cwd,
|
|
22
|
+
env: processEnv,
|
|
23
|
+
signal: opts?.signal,
|
|
24
|
+
stdio: [
|
|
25
|
+
opts?.stdin !== void 0 ? "pipe" : "ignore",
|
|
26
|
+
"pipe",
|
|
27
|
+
"pipe"
|
|
28
|
+
],
|
|
29
|
+
windowsVerbatimArguments: isWindows
|
|
30
|
+
});
|
|
31
|
+
if (opts?.stdin !== void 0) {
|
|
32
|
+
proc.stdin.write(opts.stdin);
|
|
33
|
+
proc.stdin.end();
|
|
34
|
+
}
|
|
35
|
+
proc.on("error", (error) => {
|
|
36
|
+
reject(new Error(`exec failed: ${error.message}`));
|
|
37
|
+
});
|
|
38
|
+
collectProcessOutput(
|
|
39
|
+
{
|
|
40
|
+
stdout: proc.stdout,
|
|
41
|
+
stderr: proc.stderr,
|
|
42
|
+
kill: () => proc.kill("SIGKILL"),
|
|
43
|
+
onClose: (listener) => proc.on("close", listener)
|
|
44
|
+
},
|
|
45
|
+
{ ...opts, maxOutputTailChars },
|
|
46
|
+
resolve,
|
|
47
|
+
reject
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
interactiveExec: (args, opts) => {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const [cmd, ...rest] = args;
|
|
54
|
+
const proc = spawn(cmd, rest, {
|
|
55
|
+
cwd: opts.cwd ?? worktreePath,
|
|
56
|
+
env: processEnv,
|
|
57
|
+
stdio: [opts.stdin, opts.stdout, opts.stderr],
|
|
58
|
+
shell: process.platform === "win32",
|
|
59
|
+
signal: opts.signal
|
|
60
|
+
});
|
|
61
|
+
proc.on("error", (error) => {
|
|
62
|
+
reject(new Error(`exec failed: ${error.message}`));
|
|
63
|
+
});
|
|
64
|
+
proc.on("close", (code) => {
|
|
65
|
+
resolve({ exitCode: code ?? 0 });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
close: async () => {
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return handle;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export { noSandbox };
|
|
77
|
+
//# sourceMappingURL=chunk-SOJTAJTF.js.map
|
|
78
|
+
//# sourceMappingURL=chunk-SOJTAJTF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sandboxes/no-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-SOJTAJTF.js","sourcesContent":["/**\n * No-sandbox provider — runs the agent directly on the host with no container isolation.\n *\n * Usage:\n * import { noSandbox } from \"@snappedly-tools/shipyard/sandboxes/no-sandbox\";\n * await run({ agent: codex(CODEX_MODELS.routine), sandbox: noSandbox(), prompt: \"...\" });\n *\n * Accepted by `run()`, `interactive()`, and `createSandbox()`. It skips\n * container isolation entirely and does not pass\n * `--dangerously-skip-permissions` to the agent.\n */\n\nimport { spawn, type StdioOptions } from \"node:child_process\";\nimport type {\n NoSandboxProvider,\n NoSandboxHandle,\n ExecResult,\n InteractiveExecOptions,\n} from \"../SandboxProvider.js\";\nimport { MAX_TAIL_CHARS } from \"../boundedTail.js\";\nimport { collectProcessOutput } from \"../processOutput.js\";\n\nexport interface NoSandboxOptions {\n /** Environment variables injected by this provider. Merged at launch time. */\n readonly env?: Record<string, string>;\n /**\n * Maximum number of characters of streamed `exec` output retained per stream\n * (stdout and stderr) when an `onLine` callback is supplied (default: 64KiB).\n *\n * Output is delivered live to `onLine` regardless; this only bounds the tail\n * returned in `ExecResult`, preventing a long-running agent's output from\n * overflowing V8's max string length and crashing the run.\n */\n readonly maxOutputTailChars?: number;\n}\n\n/**\n * Create a no-sandbox provider.\n *\n * The returned provider runs the agent directly on the host. All three\n * branch strategies are supported (head, merge-to-head, branch),\n * defaulting to head.\n */\nexport const noSandbox = (options?: NoSandboxOptions): NoSandboxProvider => ({\n tag: \"none\",\n name: \"no-sandbox\",\n env: options?.env ?? {},\n create: async (createOptions): Promise<NoSandboxHandle> => {\n const worktreePath = createOptions.worktreePath;\n const processEnv = { ...process.env, ...createOptions.env };\n const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;\n\n const handle: NoSandboxHandle = {\n worktreePath,\n\n exec: (\n command: string,\n opts?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n signal?: AbortSignal;\n maxOutputBytes?: number;\n },\n ): Promise<ExecResult> => {\n // sudo is a no-op for no-sandbox — the user is already on the host\n const cwd = opts?.cwd ?? worktreePath;\n const isWindows = process.platform === \"win32\";\n // PowerShell and cmd.exe don't ship `sh`, so on Windows route the\n // command string through cmd.exe instead. `/d` skips AutoRun, `/s`\n // preserves the quoted command verbatim, `/c` runs it and exits.\n // `windowsVerbatimArguments` keeps Node from re-quoting our args.\n const shellCmd = isWindows ? \"cmd.exe\" : \"sh\";\n const shellArgs = isWindows\n ? [\"/d\", \"/s\", \"/c\", command]\n : [\"-c\", command];\n\n return new Promise((resolve, reject) => {\n const proc = spawn(shellCmd, shellArgs, {\n cwd,\n env: processEnv,\n signal: opts?.signal,\n stdio: [\n opts?.stdin !== undefined ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ],\n windowsVerbatimArguments: isWindows,\n });\n\n if (opts?.stdin !== undefined) {\n proc.stdin!.write(opts.stdin);\n proc.stdin!.end();\n }\n\n proc.on(\"error\", (error) => {\n reject(new Error(`exec failed: ${error.message}`));\n });\n\n collectProcessOutput(\n {\n stdout: proc.stdout!,\n stderr: proc.stderr!,\n kill: () => proc.kill(\"SIGKILL\"),\n onClose: (listener) => proc.on(\"close\", listener),\n },\n { ...opts, maxOutputTailChars },\n resolve,\n reject,\n );\n });\n },\n\n interactiveExec: (\n args: string[],\n opts: InteractiveExecOptions,\n ): Promise<{ exitCode: number }> => {\n return new Promise((resolve, reject) => {\n const [cmd, ...rest] = args;\n // Agent CLIs on Windows are typically installed as `.cmd`/`.ps1`\n // npm wrappers; bare `spawn(\"claude\", …)` only resolves `.exe`\n // without `shell: true`, so let cmd.exe handle PATHEXT lookup.\n const proc = spawn(cmd!, rest, {\n cwd: opts.cwd ?? worktreePath,\n env: processEnv,\n stdio: [opts.stdin, opts.stdout, opts.stderr] as StdioOptions,\n shell: process.platform === \"win32\",\n signal: opts.signal,\n });\n\n proc.on(\"error\", (error: Error) => {\n reject(new Error(`exec failed: ${error.message}`));\n });\n\n proc.on(\"close\", (code: number | null) => {\n resolve({ exitCode: code ?? 0 });\n });\n });\n },\n\n close: async (): Promise<void> => {\n // No-op — no container to tear down\n },\n };\n\n return handle;\n },\n});\n"]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
4
|
import { execFile, execFileSync, spawn } from 'child_process';
|
|
3
|
-
import { resolve, join, posix
|
|
5
|
+
import { resolve, join, posix } from 'path';
|
|
4
6
|
import { promisify } from 'util';
|
|
5
7
|
import { randomUUID } from 'crypto';
|
|
6
8
|
import { Effect } from 'effect';
|
|
7
|
-
import { constants
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
import { createInterface } from 'readline';
|
|
9
|
+
import { constants } from 'fs';
|
|
10
10
|
import { rm, lstat, realpath, mkdir, readFile, access } from 'fs/promises';
|
|
11
11
|
|
|
12
12
|
var exec = promisify(execFile);
|
|
@@ -38,79 +38,6 @@ 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
|
|
114
41
|
var dockerExec = (args) => Effect.async((resume) => {
|
|
115
42
|
execFile(
|
|
116
43
|
"docker",
|
|
@@ -221,137 +148,6 @@ var removeContainer = (containerName) => Effect.gen(function* () {
|
|
|
221
148
|
var removeImage = (imageName) => Effect.gen(function* () {
|
|
222
149
|
yield* dockerExec(["rmi", imageName]);
|
|
223
150
|
});
|
|
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
|
-
};
|
|
355
151
|
var execFileAsync = promisify(execFile);
|
|
356
152
|
var RUNNER_INSTALL_METADATA = ".shipyard-install.json";
|
|
357
153
|
var RUNNER_CONTROLLER_LOCK = ".shipyard-controller.lock";
|
|
@@ -420,7 +216,7 @@ var defaultAdapters = {
|
|
|
420
216
|
}
|
|
421
217
|
},
|
|
422
218
|
signalProcess: (pid, signal) => process.kill(pid, signal),
|
|
423
|
-
pause: (milliseconds) => new Promise((
|
|
219
|
+
pause: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
|
|
424
220
|
};
|
|
425
221
|
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;
|
|
426
222
|
var lifecycleFailure = (purpose, error) => error instanceof RunnerLifecycleError ? error : new RunnerLifecycleError(
|
|
@@ -478,8 +274,12 @@ var listRemoteRunners = async (repository, repoDir, environment, adapters) => {
|
|
|
478
274
|
return parseRemoteRunners(response.stdout);
|
|
479
275
|
};
|
|
480
276
|
var validateExistingRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
277
|
+
if (!await adapters.exists(options.maskDir)) {
|
|
278
|
+
await adapters.makeDirectory(options.maskDir);
|
|
279
|
+
}
|
|
481
280
|
await assertProtectedDirectoryIdentities(
|
|
482
281
|
options.runnerDir,
|
|
282
|
+
options.maskDir,
|
|
483
283
|
adapters.inspectDirectory
|
|
484
284
|
).catch((error) => {
|
|
485
285
|
throw lifecycleFailure("Validating protected runner directories", error);
|
|
@@ -579,8 +379,12 @@ var recoverRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
|
579
379
|
if (lock) await adapters.remove(lockPath);
|
|
580
380
|
const workDir = join(options.runnerDir, RUNNER_WORK_DIR);
|
|
581
381
|
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);
|
|
582
385
|
await assertProtectedDirectoryIdentities(
|
|
583
386
|
options.runnerDir,
|
|
387
|
+
options.maskDir,
|
|
584
388
|
adapters.inspectDirectory
|
|
585
389
|
).catch((error) => {
|
|
586
390
|
throw lifecycleFailure("Validating protected runner directories", error);
|
|
@@ -732,6 +536,7 @@ var unregisterRepositoryRunner = async (metadata, repoDir, runnerDir, environmen
|
|
|
732
536
|
var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
733
537
|
const configDir = join(options.repoDir, CONFIG_DIR);
|
|
734
538
|
const runnerDir = join(configDir, RUNNER_DIR);
|
|
539
|
+
const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
|
|
735
540
|
if (!await adapters.exists(runnerDir)) {
|
|
736
541
|
throw new RunnerLifecycleError(
|
|
737
542
|
`No repository runner is installed at ${runnerDir}.`
|
|
@@ -743,6 +548,7 @@ var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
|
743
548
|
} catch (error) {
|
|
744
549
|
if (!options.force) throw error;
|
|
745
550
|
await adapters.remove(runnerDir);
|
|
551
|
+
if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
|
|
746
552
|
return {
|
|
747
553
|
removed: true,
|
|
748
554
|
forced: true,
|
|
@@ -772,12 +578,14 @@ var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
|
772
578
|
);
|
|
773
579
|
}
|
|
774
580
|
await adapters.remove(runnerDir);
|
|
581
|
+
if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
|
|
775
582
|
return {
|
|
776
583
|
removed: true,
|
|
777
584
|
forced: true,
|
|
778
585
|
manualCleanup: `Remove runner ${metadata.name} from ${metadata.repository} in GitHub Settings > Actions > Runners.`
|
|
779
586
|
};
|
|
780
587
|
}
|
|
588
|
+
if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
|
|
781
589
|
await adapters.remove(runnerDir);
|
|
782
590
|
return { removed: true, forced: false };
|
|
783
591
|
};
|
|
@@ -834,7 +642,7 @@ var docker = (options) => {
|
|
|
834
642
|
)
|
|
835
643
|
);
|
|
836
644
|
for (const dir of parentDirsToCreate) {
|
|
837
|
-
await new Promise((
|
|
645
|
+
await new Promise((resolve2, reject) => {
|
|
838
646
|
execFile(
|
|
839
647
|
"docker",
|
|
840
648
|
[
|
|
@@ -857,7 +665,7 @@ var docker = (options) => {
|
|
|
857
665
|
)
|
|
858
666
|
);
|
|
859
667
|
} else {
|
|
860
|
-
|
|
668
|
+
resolve2();
|
|
861
669
|
}
|
|
862
670
|
}
|
|
863
671
|
);
|
|
@@ -880,7 +688,7 @@ var docker = (options) => {
|
|
|
880
688
|
if (opts?.stdin !== void 0) args.push("-i");
|
|
881
689
|
if (opts?.cwd) args.push("-w", opts.cwd);
|
|
882
690
|
args.push(containerName, "sh", "-c", effectiveCommand);
|
|
883
|
-
return new Promise((
|
|
691
|
+
return new Promise((resolve2, reject) => {
|
|
884
692
|
const proc = spawn("docker", args, {
|
|
885
693
|
signal: opts?.signal,
|
|
886
694
|
stdio: [
|
|
@@ -904,13 +712,13 @@ var docker = (options) => {
|
|
|
904
712
|
onClose: (listener) => proc.on("close", listener)
|
|
905
713
|
},
|
|
906
714
|
{ ...opts, maxOutputTailChars },
|
|
907
|
-
|
|
715
|
+
resolve2,
|
|
908
716
|
reject
|
|
909
717
|
);
|
|
910
718
|
});
|
|
911
719
|
},
|
|
912
720
|
interactiveExec: (args, opts) => {
|
|
913
|
-
return new Promise((
|
|
721
|
+
return new Promise((resolve2, reject) => {
|
|
914
722
|
opts.signal?.throwIfAborted();
|
|
915
723
|
const executionId = randomUUID();
|
|
916
724
|
const pidFile = `/tmp/shipyard-interactive-${executionId}.pid`;
|
|
@@ -942,7 +750,7 @@ var docker = (options) => {
|
|
|
942
750
|
settled = true;
|
|
943
751
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
944
752
|
if ("error" in result) reject(result.error);
|
|
945
|
-
else
|
|
753
|
+
else resolve2(result);
|
|
946
754
|
};
|
|
947
755
|
const onAbort = () => {
|
|
948
756
|
if (cancelling || settled) return;
|
|
@@ -1005,7 +813,7 @@ var docker = (options) => {
|
|
|
1005
813
|
hostPath,
|
|
1006
814
|
sandboxPath
|
|
1007
815
|
),
|
|
1008
|
-
copyFileOut: (sandboxPath, hostPath) => new Promise((
|
|
816
|
+
copyFileOut: (sandboxPath, hostPath) => new Promise((resolve2, reject) => {
|
|
1009
817
|
execFile(
|
|
1010
818
|
"docker",
|
|
1011
819
|
["cp", `${containerName}:${sandboxPath}`, hostPath],
|
|
@@ -1013,7 +821,7 @@ var docker = (options) => {
|
|
|
1013
821
|
if (error) {
|
|
1014
822
|
reject(new Error(`docker cp (out) failed: ${error.message}`));
|
|
1015
823
|
} else {
|
|
1016
|
-
|
|
824
|
+
resolve2();
|
|
1017
825
|
}
|
|
1018
826
|
}
|
|
1019
827
|
);
|
|
@@ -1027,7 +835,7 @@ var docker = (options) => {
|
|
|
1027
835
|
}
|
|
1028
836
|
});
|
|
1029
837
|
};
|
|
1030
|
-
var checkImageUid = (imageName, expectedUid) => new Promise((
|
|
838
|
+
var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) => {
|
|
1031
839
|
execFile(
|
|
1032
840
|
"docker",
|
|
1033
841
|
["image", "inspect", imageName, "--format", "{{.Config.User}}"],
|
|
@@ -1042,13 +850,13 @@ var checkImageUid = (imageName, expectedUid) => new Promise((resolve3, reject) =
|
|
|
1042
850
|
}
|
|
1043
851
|
const imageUser = (stdout ?? "").toString().trim();
|
|
1044
852
|
if (!imageUser) {
|
|
1045
|
-
|
|
853
|
+
resolve2();
|
|
1046
854
|
return;
|
|
1047
855
|
}
|
|
1048
856
|
const uidPart = imageUser.split(":")[0];
|
|
1049
857
|
const imageUid = parseInt(uidPart, 10);
|
|
1050
858
|
if (isNaN(imageUid)) {
|
|
1051
|
-
|
|
859
|
+
resolve2();
|
|
1052
860
|
return;
|
|
1053
861
|
}
|
|
1054
862
|
if (imageUid !== expectedUid) {
|
|
@@ -1058,12 +866,12 @@ var checkImageUid = (imageName, expectedUid) => new Promise((resolve3, reject) =
|
|
|
1058
866
|
)
|
|
1059
867
|
);
|
|
1060
868
|
} else {
|
|
1061
|
-
|
|
869
|
+
resolve2();
|
|
1062
870
|
}
|
|
1063
871
|
}
|
|
1064
872
|
);
|
|
1065
873
|
});
|
|
1066
874
|
|
|
1067
|
-
export { REPOSITORY_RUNNER_OWNER_ENV, RUNNER_CONTROLLER_LOCK, RUNNER_INSTALL_METADATA, RUNNER_WORK_DIR, RunnerLifecycleError, buildImage,
|
|
1068
|
-
//# sourceMappingURL=chunk-
|
|
1069
|
-
//# sourceMappingURL=chunk-
|
|
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
|