@snappedly-tools/shipyard 0.4.4 → 0.5.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 +99 -146
- package/dist/chunk-56TDSWFU.js +255 -0
- package/dist/chunk-56TDSWFU.js.map +1 -0
- package/dist/chunk-ACD46ZM4.js +136 -0
- package/dist/chunk-ACD46ZM4.js.map +1 -0
- package/dist/{chunk-2GT3OPO5.js → chunk-FDYOTN55.js} +196 -6
- package/dist/chunk-FDYOTN55.js.map +1 -0
- package/dist/chunk-SOJTAJTF.js +78 -0
- package/dist/chunk-SOJTAJTF.js.map +1 -0
- package/dist/chunk-WAXEMZUV.js +877 -0
- package/dist/chunk-WAXEMZUV.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -118
- package/dist/index.js.map +1 -1
- package/dist/main.js +1831 -143
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.js +3 -3
- package/dist/sandboxes/no-sandbox.js +2 -2
- package/dist/sandboxes/vercel.js +15 -35
- package/dist/sandboxes/vercel.js.map +1 -1
- package/dist/templates/parallel-planner/main.mts +11 -8
- package/dist/templates/parallel-planner-with-review/main.mts +23 -13
- package/package.json +3 -2
- package/dist/chunk-2GT3OPO5.js.map +0 -1
- package/dist/chunk-OCJKONZ7.js +0 -475
- package/dist/chunk-OCJKONZ7.js.map +0 -1
- package/dist/chunk-VDHZO5PP.js +0 -113
- package/dist/chunk-VDHZO5PP.js.map +0 -1
- package/dist/chunk-VW4MI5GJ.js +0 -126
- package/dist/chunk-VW4MI5GJ.js.map +0 -1
- package/dist/chunk-WA7QPAXO.js +0 -50
- package/dist/chunk-WA7QPAXO.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,204 +4,157 @@
|
|
|
4
4
|
<img src="assets/brand/shipyard-robot-on-boat.png" alt="A friendly robot steering a blue boat for Shipyard" width="320">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
> **
|
|
7
|
+
> **Fully autonomous coding workflows, protected by Docker.**
|
|
8
8
|
|
|
9
9
|
[](https://github.com/snappedly/shipyard/actions/workflows/ci.yml)
|
|
10
10
|
|
|
11
|
-
Shipyard
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Give Shipyard a backlog and a policy. It plans dependencies, runs AI coding
|
|
12
|
+
agents in isolated sandboxes, reviews their work, and returns reviewable
|
|
13
|
+
commits—without babysitting.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## The Shipyard advantage
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **Purpose-built agents.** Use Codex or Claude Code without carrying support
|
|
20
|
-
for unrelated coding-agent CLIs.
|
|
21
|
-
- **Reviewable changes.** Control how branches and worktrees move changes back
|
|
22
|
-
to the host repository.
|
|
17
|
+
Most agent setups launch a CLI and hope for the best. Shipyard is the control
|
|
18
|
+
plane around the agent:
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
- **Protected execution:** Run Codex or Claude Code in an isolated Docker-backed
|
|
21
|
+
sandbox with explicit mounts, credentials, and network access.
|
|
22
|
+
- **Real orchestration:** Plan dependencies, parallelize safe work, leave
|
|
23
|
+
blocked work alone, review each branch, and merge completed work.
|
|
24
|
+
- **Failure-aware by design:** Use finite budgets, no-progress detection,
|
|
25
|
+
cancellation, logs, and recovery artifacts instead of runaway loops.
|
|
26
|
+
- **Reviewable output:** Runs preserve branches, worktrees, logs, and evidence;
|
|
27
|
+
completed work comes back as commits. You keep control of the repository and
|
|
28
|
+
the final release.
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
## Install and run
|
|
31
|
+
|
|
32
|
+
Requirements: Node.js 20.18.1+, Git, Docker, and credentials for your chosen
|
|
33
|
+
agent. Run these commands in the repository Shipyard should change:
|
|
29
34
|
|
|
30
35
|
```sh
|
|
31
36
|
npm install --save-dev @snappedly-tools/shipyard
|
|
32
37
|
npx shipyard init
|
|
33
|
-
cp .shipyard/.env.example .shipyard/.env
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
`init` asks
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
template. Put any requested credentials in `.shipyard/.env`, then write one
|
|
40
|
-
concrete task in `.shipyard/prompt.md`.
|
|
40
|
+
`init` asks for the agent, authentication, sandbox, issue tracker, and workflow
|
|
41
|
+
template, then creates `.shipyard/`. For the first run, choose Docker and
|
|
42
|
+
`blank` or `sequential-reviewer`.
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
Add the requested credentials to `.shipyard/.env`, write a task in
|
|
45
|
+
`.shipyard/prompt.md` when using the `blank` template, then run:
|
|
43
46
|
|
|
44
47
|
```sh
|
|
45
48
|
npx shipyard run
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
image when its definition has not changed:
|
|
51
|
+
The first run builds the Docker image automatically. Reuse it when the
|
|
52
|
+
Dockerfile has not changed:
|
|
51
53
|
|
|
52
54
|
```sh
|
|
53
55
|
npx shipyard run --skip-build
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
For Codex, `init` can sign in with a ChatGPT subscription or configure an OpenAI
|
|
59
|
+
API key. Claude Code supports a subscription token or an Anthropic API key. See
|
|
60
|
+
the [agent guide](docs/content/docs/agents.mdx) for authentication details.
|
|
59
61
|
|
|
60
|
-
##
|
|
62
|
+
## Pick the workflow you need
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
| Template | Best for | Built-in flow |
|
|
65
|
+
| ------------------------------ | --------------------- | ------------------------------------------------- |
|
|
66
|
+
| `blank` | One custom task | One agent run |
|
|
67
|
+
| `simple-loop` | A small issue backlog | Implement issues sequentially |
|
|
68
|
+
| `sequential-reviewer` | Safer issue delivery | Implement → review, one issue at a time |
|
|
69
|
+
| `parallel-planner` | Independent issues | Plan dependencies → implement in parallel → merge |
|
|
70
|
+
| `parallel-planner-with-review` | Maximum autonomy | Plan → implement and review in parallel → merge |
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
All templates are generated TypeScript. Adjust prompts, models, iteration
|
|
73
|
+
limits, branch strategy, hooks, and checks in `.shipyard/main.ts` or
|
|
74
|
+
`.shipyard/main.mts`.
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
agent: codex(CODEX_MODELS.routine),
|
|
71
|
-
sandbox: docker(),
|
|
72
|
-
promptFile: ".shipyard/prompt.md",
|
|
73
|
-
});
|
|
76
|
+
## Keep a repository running
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
On Apple Silicon macOS, the optional repository runner keeps a foreground
|
|
79
|
+
Shipyard controller ready for GitHub Issues. Label an issue `shipyard`; the
|
|
80
|
+
controller wakes, drains a finite batch of eligible work, coalesces duplicate
|
|
81
|
+
wake-ups, and stops when it makes no progress. Restarting it recovers work
|
|
82
|
+
labelled while the host was offline.
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
Accept runner installation during `init`, or install it later:
|
|
79
85
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
maxIterations: 1,
|
|
87
|
-
logging: { type: "stdout" },
|
|
88
|
-
});
|
|
86
|
+
```sh
|
|
87
|
+
npx shipyard runner install
|
|
88
|
+
git add .github/workflows/shipyard-wake.yml .shipyard/.gitignore
|
|
89
|
+
git commit -m "Add Shipyard wake workflow"
|
|
90
|
+
git push
|
|
91
|
+
npx shipyard runner start
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
Run `npx shipyard runner purge` to remove every dated run-log folder and
|
|
95
|
+
root-level `.log` file under `.shipyard/logs/`. Shipyard automatically removes
|
|
96
|
+
entries older than eight days before `shipyard run`, at runner startup, and
|
|
97
|
+
daily while the runner stays active. Use a path outside `.shipyard/logs/` for
|
|
98
|
+
logs that must be retained separately.
|
|
99
|
+
|
|
100
|
+
See the [repository runner guide](docs/content/docs/repository-runner.mdx) for
|
|
101
|
+
requirements, lifecycle commands, and the security model.
|
|
94
102
|
|
|
95
|
-
|
|
103
|
+
## Build your own coordinator
|
|
104
|
+
|
|
105
|
+
Shipyard is also a TypeScript library. Compose your own workflow with
|
|
106
|
+
`run()`, `createSandbox()`, and `createWorktree()` while reusing its agent,
|
|
107
|
+
sandbox, branch, prompt, logging, cancellation, and session primitives.
|
|
96
108
|
|
|
97
109
|
```ts
|
|
98
110
|
import { CODEX_MODELS, codex, run } from "@snappedly-tools/shipyard";
|
|
99
|
-
import {
|
|
111
|
+
import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
await run({
|
|
102
114
|
agent: codex(CODEX_MODELS.routine),
|
|
103
|
-
sandbox:
|
|
115
|
+
sandbox: docker(),
|
|
104
116
|
promptFile: ".shipyard/prompt.md",
|
|
105
117
|
});
|
|
106
118
|
```
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
## Branch behavior
|
|
112
|
-
|
|
113
|
-
Docker keeps the repository and Git metadata inside the sandbox. It starts
|
|
114
|
-
from committed history and syncs changes back. The generated blank
|
|
115
|
-
template omits `branchStrategy`, which defaults to `merge-to-head`: commits
|
|
116
|
-
are transferred to a temporary host branch and merged into your current
|
|
117
|
-
branch. Direct `head` mode is not supported by Docker or Vercel Sandbox.
|
|
118
|
-
|
|
119
|
-
The explicit `branch` strategy keeps commits on the named branch for review.
|
|
120
|
-
Commit input files before either strategy, or use `copyToWorktree` for specific
|
|
121
|
-
untracked or ignored files.
|
|
122
|
-
|
|
123
|
-
## Authentication
|
|
124
|
-
|
|
125
|
-
`shipyard init` generates the environment variables and mounts required by
|
|
126
|
-
the chosen agent. Keep `.shipyard/.env`, agent login files, logs, and recovery
|
|
127
|
-
patches private.
|
|
128
|
-
|
|
129
|
-
Use a subscription first when the selected agent supports it. If subscription
|
|
130
|
-
authentication does not work in your environment, use the API-key fallback:
|
|
131
|
-
|
|
132
|
-
- Codex subscription authentication: choose **Sign in with ChatGPT** during
|
|
133
|
-
interactive init and complete the browser login. The generated configuration
|
|
134
|
-
mounts `~/.codex/auth.json` read-only. For non-interactive init, run
|
|
135
|
-
`codex login` on the host, then pass `--codex-auth chatgpt`.
|
|
136
|
-
- Codex API authentication: choose **OpenAI API key** during interactive init
|
|
137
|
-
and put `OPENAI_API_KEY` in `.shipyard/.env`. For non-interactive init, pass
|
|
138
|
-
`--codex-auth api-key`.
|
|
139
|
-
- Claude Code subscription authentication: run `claude setup-token` on the
|
|
140
|
-
host and put the result in `CLAUDE_CODE_OAUTH_TOKEN` in `.shipyard/.env`.
|
|
141
|
-
- Claude Code API authentication: uncomment `ANTHROPIC_API_KEY` in
|
|
142
|
-
`.shipyard/.env` and put your API key there.
|
|
143
|
-
- GitHub Issues authentication: set `GH_TOKEN` in `.shipyard/.env`. To use the
|
|
144
|
-
token from a host-side `gh auth login` without storing it in the file, leave
|
|
145
|
-
`GH_TOKEN=` blank and run `GH_TOKEN="$(gh auth token)" npx shipyard run`.
|
|
146
|
-
|
|
147
|
-
## Generated files
|
|
120
|
+
Docker is the default path. Vercel Sandbox is available for isolated cloud
|
|
121
|
+
execution; `noSandbox()` is available only for trusted repositories and prompts.
|
|
148
122
|
|
|
149
|
-
|
|
150
|
-
| --------------------------------- | ------------------------------------------------------------ |
|
|
151
|
-
| `.shipyard/main.ts` or `main.mts` | Agent, sandbox, prompt, branch, hook, and iteration settings |
|
|
152
|
-
| `.shipyard/prompt.md` | Task given to the agent |
|
|
153
|
-
| `.shipyard/Dockerfile` | Sandbox image definition |
|
|
154
|
-
| `.shipyard/.env` | Untracked credentials passed into the sandbox |
|
|
155
|
-
| `.shipyard/logs/` | Run logs |
|
|
156
|
-
| `.shipyard/worktrees/` | Worktrees for separate-branch runs |
|
|
157
|
-
| `.shipyard/patches/` | Recovery artifacts preserved after some failures |
|
|
123
|
+
## What gets created
|
|
158
124
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Read generated prompts before running an issue or merge workflow. Those
|
|
170
|
-
templates can close issues, create branches, and merge work.
|
|
171
|
-
|
|
172
|
-
The package also exports contracts for triage, implementation, review,
|
|
173
|
-
repair, handoff, and release recording. They are building blocks for hosted
|
|
174
|
-
automation, not a hosted service started by the quick-start commands.
|
|
175
|
-
|
|
176
|
-
## Security
|
|
177
|
-
|
|
178
|
-
Agents receive the permissions granted by their sandbox, mounts, credentials,
|
|
179
|
-
and hooks. Docker isolates repository and Git storage; it does not
|
|
180
|
-
mount the host checkout or Git metadata automatically. Explicit mounts must
|
|
181
|
-
stay outside the sandbox workspace and its ancestors.
|
|
125
|
+
```text
|
|
126
|
+
.shipyard/
|
|
127
|
+
├── main.ts or main.mts # workflow configuration
|
|
128
|
+
├── prompt.md # task or issue instructions
|
|
129
|
+
├── Dockerfile # sandbox image definition
|
|
130
|
+
├── .env # untracked credentials
|
|
131
|
+
├── logs/ # run logs
|
|
132
|
+
├── worktrees/ # isolated branch worktrees
|
|
133
|
+
└── patches/ # recovery artifacts
|
|
134
|
+
```
|
|
182
135
|
|
|
183
|
-
|
|
184
|
-
still broaden an agent's access. Host hooks, no-sandbox mode, and custom
|
|
185
|
-
provider code retain their own trust requirements. Use
|
|
186
|
-
least-privilege credentials and review generated configuration before running
|
|
187
|
-
untrusted code.
|
|
136
|
+
## Security and control
|
|
188
137
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
138
|
+
Docker keeps the agent's repository and Git storage inside the sandbox by
|
|
139
|
+
default. It is not a magic security boundary: mounts, credentials, devices,
|
|
140
|
+
network access, host hooks, and `noSandbox()` can expand what an agent can do.
|
|
141
|
+
Use least-privilege credentials and review generated configuration before
|
|
142
|
+
running untrusted code. Read the [security evaluation](docs/security-evaluation.md)
|
|
143
|
+
and [SECURITY.md](SECURITY.md).
|
|
192
144
|
|
|
193
|
-
|
|
145
|
+
Shipyard runs in your infrastructure. There is no required hosted control
|
|
146
|
+
plane; you control the host, Docker, model access, logs, and release policy.
|
|
194
147
|
|
|
195
|
-
|
|
148
|
+
## Learn more
|
|
196
149
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
150
|
+
- [Getting started](docs/content/docs/index.mdx)
|
|
151
|
+
- [Configuration](docs/content/docs/configuration.mdx)
|
|
152
|
+
- [Repository runner operations](docs/runbooks/repository-runner-macos-validation.md)
|
|
153
|
+
- [Contributing](CONTRIBUTING.md)
|
|
201
154
|
|
|
202
155
|
## License
|
|
203
156
|
|
|
204
157
|
Shipyard is source-available under the
|
|
205
158
|
[PolyForm Strict License 1.0.0](LICENSE). It is available for permitted
|
|
206
|
-
noncommercial use; the license does not permit redistribution, modification,
|
|
207
|
-
|
|
159
|
+
noncommercial use; the license does not permit redistribution, modification, or
|
|
160
|
+
derivative works. Contact Snappedly to request a commercial license.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { CONFIG_DIR, ConfigDirError, assertNoSymlinkComponents, LOGS_DIR, Display } from './chunk-FDYOTN55.js';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { FileSystem } from '@effect/platform';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { readdir, lstat, rm } from 'fs/promises';
|
|
6
|
+
|
|
7
|
+
var formatErrorMessage = (error) => {
|
|
8
|
+
switch (error._tag) {
|
|
9
|
+
case "ExecError":
|
|
10
|
+
return `Command failed in sandbox (${error.command}): ${error.message}`;
|
|
11
|
+
case "ExecHostError":
|
|
12
|
+
return `Command failed on host (${error.command}): ${error.message}`;
|
|
13
|
+
case "CopyError":
|
|
14
|
+
return `File copy failed: ${error.message}`;
|
|
15
|
+
case "DockerError":
|
|
16
|
+
return `Docker operation failed: ${error.message}. Is Docker running?`;
|
|
17
|
+
case "SyncError":
|
|
18
|
+
return `Git sync failed: ${error.message}`;
|
|
19
|
+
case "WorktreeError":
|
|
20
|
+
return `Git worktree operation failed: ${error.message}`;
|
|
21
|
+
case "PromptError":
|
|
22
|
+
return `Failed to resolve prompt: ${error.message}`;
|
|
23
|
+
case "AgentError":
|
|
24
|
+
return `Agent invocation failed: ${error.message}`;
|
|
25
|
+
case "ConfigDirError":
|
|
26
|
+
return `${error.message}`;
|
|
27
|
+
case "InitError":
|
|
28
|
+
return `${error.message}`;
|
|
29
|
+
case "AgentIdleTimeoutError":
|
|
30
|
+
case "WorktreeTimeoutError":
|
|
31
|
+
case "ContainerStartTimeoutError":
|
|
32
|
+
case "CopyToWorktreeTimeoutError":
|
|
33
|
+
case "CopyToWorktreeError":
|
|
34
|
+
case "SyncInTimeoutError":
|
|
35
|
+
case "HookTimeoutError":
|
|
36
|
+
case "GitSetupTimeoutError":
|
|
37
|
+
case "PromptExpansionTimeoutError":
|
|
38
|
+
case "CommitCollectionTimeoutError":
|
|
39
|
+
case "MergeToHostTimeoutError":
|
|
40
|
+
case "SessionCaptureError":
|
|
41
|
+
case "CwdError":
|
|
42
|
+
return error.message;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var showErrorAndExit = (error) => Effect.gen(function* () {
|
|
46
|
+
const d = yield* Display;
|
|
47
|
+
yield* d.status(formatErrorMessage(error), "error");
|
|
48
|
+
return yield* Effect.sync(() => process.exit(1));
|
|
49
|
+
});
|
|
50
|
+
var withFriendlyErrors = (effect) => (
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
Effect.catchTags(effect, {
|
|
53
|
+
ExecError: showErrorAndExit,
|
|
54
|
+
ExecHostError: showErrorAndExit,
|
|
55
|
+
CopyError: showErrorAndExit,
|
|
56
|
+
DockerError: showErrorAndExit,
|
|
57
|
+
SyncError: showErrorAndExit,
|
|
58
|
+
WorktreeError: showErrorAndExit,
|
|
59
|
+
PromptError: showErrorAndExit,
|
|
60
|
+
AgentError: showErrorAndExit,
|
|
61
|
+
ConfigDirError: showErrorAndExit,
|
|
62
|
+
InitError: showErrorAndExit,
|
|
63
|
+
AgentIdleTimeoutError: showErrorAndExit,
|
|
64
|
+
WorktreeTimeoutError: showErrorAndExit,
|
|
65
|
+
ContainerStartTimeoutError: showErrorAndExit,
|
|
66
|
+
CopyToWorktreeTimeoutError: showErrorAndExit,
|
|
67
|
+
CopyToWorktreeError: showErrorAndExit,
|
|
68
|
+
SyncInTimeoutError: showErrorAndExit,
|
|
69
|
+
HookTimeoutError: showErrorAndExit,
|
|
70
|
+
GitSetupTimeoutError: showErrorAndExit,
|
|
71
|
+
PromptExpansionTimeoutError: showErrorAndExit,
|
|
72
|
+
CommitCollectionTimeoutError: showErrorAndExit,
|
|
73
|
+
MergeToHostTimeoutError: showErrorAndExit,
|
|
74
|
+
SessionCaptureError: showErrorAndExit,
|
|
75
|
+
CwdError: showErrorAndExit
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// src/modelConfig.ts
|
|
80
|
+
var CODEX_REASONING_EFFORTS = [
|
|
81
|
+
"low",
|
|
82
|
+
"medium",
|
|
83
|
+
"high",
|
|
84
|
+
"xhigh",
|
|
85
|
+
"max"
|
|
86
|
+
];
|
|
87
|
+
var DEFAULT_CODEX_MODELS = {
|
|
88
|
+
routine: { model: "gpt-5.6-luna", effort: "max" },
|
|
89
|
+
strong: { model: "gpt-5.6-sol", effort: "max" }
|
|
90
|
+
};
|
|
91
|
+
var readModel = (role) => process.env[`SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`]?.trim() || DEFAULT_CODEX_MODELS[role].model;
|
|
92
|
+
var readEffort = (role) => {
|
|
93
|
+
const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
|
|
94
|
+
const value = process.env[envName]?.trim();
|
|
95
|
+
if (!value) return DEFAULT_CODEX_MODELS[role].effort;
|
|
96
|
+
if (CODEX_REASONING_EFFORTS.includes(value)) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${value}"`
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
var CODEX_MODELS = {
|
|
104
|
+
routine: {
|
|
105
|
+
model: readModel("routine"),
|
|
106
|
+
effort: readEffort("routine")
|
|
107
|
+
},
|
|
108
|
+
strong: {
|
|
109
|
+
model: readModel("strong"),
|
|
110
|
+
effort: readEffort("strong")
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var parseEnvFile = (filePath) => Effect.gen(function* () {
|
|
114
|
+
const fs = yield* FileSystem.FileSystem;
|
|
115
|
+
const symlink = yield* fs.readLink(filePath).pipe(
|
|
116
|
+
Effect.map(() => true),
|
|
117
|
+
Effect.catchAll(() => Effect.succeed(false))
|
|
118
|
+
);
|
|
119
|
+
if (symlink) return {};
|
|
120
|
+
const content = yield* fs.readFileString(filePath).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
121
|
+
if (content === null) return {};
|
|
122
|
+
const vars = {};
|
|
123
|
+
for (const line of content.split("\n")) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
126
|
+
const eqIndex = trimmed.indexOf("=");
|
|
127
|
+
if (eqIndex === -1) continue;
|
|
128
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
129
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
130
|
+
const isDoubleQuoted = value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"';
|
|
131
|
+
const isSingleQuoted = value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'";
|
|
132
|
+
if (isDoubleQuoted || isSingleQuoted) {
|
|
133
|
+
value = value.slice(1, -1);
|
|
134
|
+
}
|
|
135
|
+
if (isDoubleQuoted) {
|
|
136
|
+
value = value.replace(/\\([nrt\\])/g, (_, ch) => {
|
|
137
|
+
const escapes = {
|
|
138
|
+
n: "\n",
|
|
139
|
+
r: "\r",
|
|
140
|
+
t: " ",
|
|
141
|
+
"\\": "\\"
|
|
142
|
+
};
|
|
143
|
+
return escapes[ch] ?? ch;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
vars[key] = value;
|
|
147
|
+
}
|
|
148
|
+
return vars;
|
|
149
|
+
});
|
|
150
|
+
var resolveEnv = (repoDir) => Effect.gen(function* () {
|
|
151
|
+
const configDir = join(repoDir, CONFIG_DIR);
|
|
152
|
+
const envPath = join(configDir, ".env");
|
|
153
|
+
yield* Effect.tryPromise({
|
|
154
|
+
try: () => assertNoSymlinkComponents(repoDir, envPath, "environment file"),
|
|
155
|
+
catch: (e) => new ConfigDirError({
|
|
156
|
+
message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
const shipyardEnv = yield* parseEnvFile(envPath);
|
|
160
|
+
const result = {};
|
|
161
|
+
for (const key of Object.keys(shipyardEnv)) {
|
|
162
|
+
const value = shipyardEnv[key] || process.env[key];
|
|
163
|
+
if (value) {
|
|
164
|
+
result[key] = value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
});
|
|
169
|
+
var DEFAULT_LOG_RETENTION_DAYS = 8;
|
|
170
|
+
var LOG_DIRECTORY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
171
|
+
var buildLogDirectoryName = (date = /* @__PURE__ */ new Date()) => {
|
|
172
|
+
if (Number.isNaN(date.getTime())) {
|
|
173
|
+
throw new Error("Cannot build a log directory name from an invalid date.");
|
|
174
|
+
}
|
|
175
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
176
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
177
|
+
};
|
|
178
|
+
var parseLogDirectoryDate = (name) => {
|
|
179
|
+
const match = LOG_DIRECTORY_PATTERN.exec(name);
|
|
180
|
+
if (!match) return void 0;
|
|
181
|
+
const year = Number(match[1]);
|
|
182
|
+
const month = Number(match[2]);
|
|
183
|
+
const day = Number(match[3]);
|
|
184
|
+
const date = new Date(year, month - 1, day);
|
|
185
|
+
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : void 0;
|
|
186
|
+
};
|
|
187
|
+
var validateRetentionDays = (retentionDays) => {
|
|
188
|
+
if (!Number.isSafeInteger(retentionDays) || retentionDays < 0) {
|
|
189
|
+
throw new Error("Log retention days must be a non-negative integer.");
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var purgeRunLogs = async (options) => {
|
|
193
|
+
const retentionDays = options.retentionDays;
|
|
194
|
+
if (retentionDays !== void 0) validateRetentionDays(retentionDays);
|
|
195
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
196
|
+
if (retentionDays !== void 0 && Number.isNaN(now.getTime())) {
|
|
197
|
+
throw new Error("Cannot purge logs using an invalid date.");
|
|
198
|
+
}
|
|
199
|
+
const cutoff = retentionDays === void 0 ? void 0 : new Date(
|
|
200
|
+
now.getFullYear(),
|
|
201
|
+
now.getMonth(),
|
|
202
|
+
now.getDate() - retentionDays
|
|
203
|
+
);
|
|
204
|
+
const cutoffDate = cutoff === void 0 ? void 0 : buildLogDirectoryName(cutoff);
|
|
205
|
+
const logsDir = join(options.repoDir, CONFIG_DIR, LOGS_DIR);
|
|
206
|
+
let entries;
|
|
207
|
+
try {
|
|
208
|
+
entries = await readdir(logsDir, { withFileTypes: true });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
211
|
+
return {
|
|
212
|
+
logsDir,
|
|
213
|
+
...retentionDays === void 0 ? {} : { retentionDays, cutoffDate },
|
|
214
|
+
scannedDirectories: 0,
|
|
215
|
+
removedCount: 0,
|
|
216
|
+
removedDirectories: [],
|
|
217
|
+
removedFiles: []
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
const removedDirectories = [];
|
|
223
|
+
const removedFiles = [];
|
|
224
|
+
let scannedDirectories = 0;
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const entryPath = join(logsDir, entry.name);
|
|
227
|
+
if (entry.isFile() && entry.name.endsWith(".log")) {
|
|
228
|
+
const file = await lstat(entryPath);
|
|
229
|
+
if (file.isFile() && (cutoff === void 0 || file.mtime < cutoff)) {
|
|
230
|
+
await rm(entryPath, { force: true });
|
|
231
|
+
removedFiles.push(entry.name);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!entry.isDirectory()) continue;
|
|
236
|
+
const directoryDate = parseLogDirectoryDate(entry.name);
|
|
237
|
+
if (directoryDate === void 0) continue;
|
|
238
|
+
scannedDirectories += 1;
|
|
239
|
+
if (cutoff !== void 0 && directoryDate >= cutoff) continue;
|
|
240
|
+
await rm(entryPath, { recursive: true, force: true });
|
|
241
|
+
removedDirectories.push(entry.name);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
logsDir,
|
|
245
|
+
...retentionDays === void 0 ? {} : { retentionDays, cutoffDate },
|
|
246
|
+
scannedDirectories,
|
|
247
|
+
removedCount: removedDirectories.length + removedFiles.length,
|
|
248
|
+
removedDirectories,
|
|
249
|
+
removedFiles
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export { CODEX_MODELS, CODEX_REASONING_EFFORTS, DEFAULT_LOG_RETENTION_DAYS, buildLogDirectoryName, formatErrorMessage, purgeRunLogs, resolveEnv, withFriendlyErrors };
|
|
254
|
+
//# sourceMappingURL=chunk-56TDSWFU.js.map
|
|
255
|
+
//# sourceMappingURL=chunk-56TDSWFU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ErrorHandler.ts","../src/modelConfig.ts","../src/EnvResolver.ts","../src/LogRetention.ts"],"names":["Effect","join"],"mappings":"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","file":"chunk-56TDSWFU.js","sourcesContent":["import { Effect } from \"effect\";\nimport { Display } from \"./Display.js\";\nimport type { SandboxError } from \"./errors.js\";\n\n/**\n * Formats a tagged SandboxError into a user-friendly message with\n * context-specific hints about what went wrong and how to recover.\n */\nexport const formatErrorMessage = (error: SandboxError): string => {\n switch (error._tag) {\n case \"ExecError\":\n return `Command failed in sandbox (${error.command}): ${error.message}`;\n case \"ExecHostError\":\n return `Command failed on host (${error.command}): ${error.message}`;\n case \"CopyError\":\n return `File copy failed: ${error.message}`;\n case \"DockerError\":\n return `Docker operation failed: ${error.message}. Is Docker running?`;\n case \"SyncError\":\n return `Git sync failed: ${error.message}`;\n case \"WorktreeError\":\n return `Git worktree operation failed: ${error.message}`;\n case \"PromptError\":\n return `Failed to resolve prompt: ${error.message}`;\n case \"AgentError\":\n return `Agent invocation failed: ${error.message}`;\n case \"ConfigDirError\":\n return `${error.message}`;\n case \"InitError\":\n return `${error.message}`;\n case \"AgentIdleTimeoutError\":\n case \"WorktreeTimeoutError\":\n case \"ContainerStartTimeoutError\":\n case \"CopyToWorktreeTimeoutError\":\n case \"CopyToWorktreeError\":\n case \"SyncInTimeoutError\":\n case \"HookTimeoutError\":\n case \"GitSetupTimeoutError\":\n case \"PromptExpansionTimeoutError\":\n case \"CommitCollectionTimeoutError\":\n case \"MergeToHostTimeoutError\":\n case \"SessionCaptureError\":\n case \"CwdError\":\n return error.message;\n }\n};\n\nconst showErrorAndExit = (error: SandboxError) =>\n Effect.gen(function* () {\n const d = yield* Display;\n yield* d.status(formatErrorMessage(error), \"error\");\n return yield* Effect.sync(() => process.exit(1) as never);\n });\n\n/**\n * Wraps an effect so that any SandboxError is caught, displayed via the\n * Display service as an error-severity status message, then exits the process\n * with code 1. Non-SandboxError errors pass through unchanged.\n */\nexport const withFriendlyErrors = <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, Exclude<E, SandboxError>, R | Display> =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Effect.catchTags(effect as Effect.Effect<A, SandboxError, R>, {\n ExecError: showErrorAndExit,\n ExecHostError: showErrorAndExit,\n CopyError: showErrorAndExit,\n DockerError: showErrorAndExit,\n SyncError: showErrorAndExit,\n WorktreeError: showErrorAndExit,\n PromptError: showErrorAndExit,\n AgentError: showErrorAndExit,\n ConfigDirError: showErrorAndExit,\n InitError: showErrorAndExit,\n AgentIdleTimeoutError: showErrorAndExit,\n WorktreeTimeoutError: showErrorAndExit,\n ContainerStartTimeoutError: showErrorAndExit,\n CopyToWorktreeTimeoutError: showErrorAndExit,\n CopyToWorktreeError: showErrorAndExit,\n SyncInTimeoutError: showErrorAndExit,\n HookTimeoutError: showErrorAndExit,\n GitSetupTimeoutError: showErrorAndExit,\n PromptExpansionTimeoutError: showErrorAndExit,\n CommitCollectionTimeoutError: showErrorAndExit,\n MergeToHostTimeoutError: showErrorAndExit,\n SessionCaptureError: showErrorAndExit,\n CwdError: showErrorAndExit,\n }) as Effect.Effect<A, Exclude<E, SandboxError>, R | Display>;\n","/** Reasoning efforts accepted by the Codex CLI. */\nexport const CODEX_REASONING_EFFORTS = [\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\",\n] as const;\n\nexport type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];\n\n/** A model identifier plus the reasoning policy used for that model role. */\nexport interface CodexModelConfig {\n readonly model: string;\n readonly effort: CodexReasoningEffort;\n}\n\n/**\n * Default values for Shipyard's built-in Codex roles.\n *\n * Runtime environment variables override these defaults:\n * SHIPYARD_CODEX_ROUTINE_MODEL\n * SHIPYARD_CODEX_ROUTINE_REASONING_EFFORT\n * SHIPYARD_CODEX_STRONG_MODEL\n * SHIPYARD_CODEX_STRONG_REASONING_EFFORT\n *\n * Keep the defaults here so runtime providers, generated templates, and the\n * repository workflows all use the same role definitions.\n */\nconst DEFAULT_CODEX_MODELS = {\n routine: { model: \"gpt-5.6-luna\", effort: \"max\" },\n strong: { model: \"gpt-5.6-sol\", effort: \"max\" },\n} as const satisfies Record<string, CodexModelConfig>;\n\nconst readModel = (role: keyof typeof DEFAULT_CODEX_MODELS): string =>\n process.env[`SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`]?.trim() ||\n DEFAULT_CODEX_MODELS[role].model;\n\nconst readEffort = (\n role: keyof typeof DEFAULT_CODEX_MODELS,\n): CodexReasoningEffort => {\n const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;\n const value = process.env[envName]?.trim();\n if (!value) return DEFAULT_CODEX_MODELS[role].effort;\n if ((CODEX_REASONING_EFFORTS as readonly string[]).includes(value)) {\n return value as CodexReasoningEffort;\n }\n throw new Error(\n `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(\", \")}; received \"${value}\"`,\n );\n};\n\nexport const CODEX_MODELS = {\n routine: {\n model: readModel(\"routine\"),\n effort: readEffort(\"routine\"),\n },\n strong: {\n model: readModel(\"strong\"),\n effort: readEffort(\"strong\"),\n },\n} as const satisfies Record<string, CodexModelConfig>;\n","import { FileSystem } from \"@effect/platform\";\nimport { Effect } from \"effect\";\nimport { join } from \"node:path\";\n\nimport { ConfigDirError } from \"./errors.js\";\nimport { assertNoSymlinkComponents } from \"./pathSecurity.js\";\nimport { CONFIG_DIR } from \"./runtimeNames.js\";\n\nconst parseEnvFile = (\n filePath: string,\n): Effect.Effect<Record<string, string>, never, FileSystem.FileSystem> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const symlink = yield* fs.readLink(filePath).pipe(\n Effect.map(() => true),\n Effect.catchAll(() => Effect.succeed(false)),\n );\n // A repository-controlled symlink could redirect this read to a host\n // secret and then expose matching keys to the agent.\n if (symlink) return {};\n const content = yield* fs\n .readFileString(filePath)\n .pipe(Effect.catchAll(() => Effect.succeed(null)));\n if (content === null) return {};\n const vars: Record<string, string> = {};\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex).trim();\n let value = trimmed.slice(eqIndex + 1).trim();\n const isDoubleQuoted =\n value.length >= 2 &&\n value[0] === '\"' &&\n value[value.length - 1] === '\"';\n const isSingleQuoted =\n value.length >= 2 &&\n value[0] === \"'\" &&\n value[value.length - 1] === \"'\";\n if (isDoubleQuoted || isSingleQuoted) {\n value = value.slice(1, -1);\n }\n if (isDoubleQuoted) {\n value = value.replace(/\\\\([nrt\\\\])/g, (_, ch: string) => {\n const escapes: Record<string, string> = {\n n: \"\\n\",\n r: \"\\r\",\n t: \"\\t\",\n \"\\\\\": \"\\\\\",\n };\n return escapes[ch] ?? ch;\n });\n }\n vars[key] = value;\n }\n return vars;\n });\n\n/**\n * Resolve all env vars from .env files with process.env fallback.\n *\n * Precedence: .shipyard/.env > process.env\n * Only keys declared in .shipyard/.env are resolved from process.env.\n * Repo root .env is not part of the resolution chain.\n */\nexport const resolveEnv = (\n repoDir: string,\n): Effect.Effect<\n Record<string, string>,\n ConfigDirError,\n FileSystem.FileSystem\n> =>\n Effect.gen(function* () {\n const configDir = join(repoDir, CONFIG_DIR);\n const envPath = join(configDir, \".env\");\n yield* Effect.tryPromise({\n try: () =>\n assertNoSymlinkComponents(repoDir, envPath, \"environment file\"),\n catch: (e) =>\n new ConfigDirError({\n message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`,\n }),\n });\n const shipyardEnv = yield* parseEnvFile(envPath);\n\n const result: Record<string, string> = {};\n for (const key of Object.keys(shipyardEnv)) {\n // A blank placeholder falls back to process.env, so callers can source\n // credentials from a host login for one invocation, e.g.\n // `GH_TOKEN=\"$(gh auth token)\" npx shipyard run`. Only keys declared in\n // .shipyard/.env are eligible for this fallback.\n const value = shipyardEnv[key] || process.env[key];\n if (value) {\n result[key] = value;\n }\n }\n\n return result;\n });\n","import { lstat, readdir, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR, LOGS_DIR } from \"./runtimeNames.js\";\n\n/** The default age threshold for automatic run-log purges. */\nexport const DEFAULT_LOG_RETENTION_DAYS = 8;\n\nconst LOG_DIRECTORY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/** Build the local calendar date directory used for default run logs. */\nexport const buildLogDirectoryName = (date: Date = new Date()): string => {\n if (Number.isNaN(date.getTime())) {\n throw new Error(\"Cannot build a log directory name from an invalid date.\");\n }\n const pad = (value: number): string => String(value).padStart(2, \"0\");\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n};\n\nconst parseLogDirectoryDate = (name: string): Date | undefined => {\n const match = LOG_DIRECTORY_PATTERN.exec(name);\n if (!match) return undefined;\n\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n const date = new Date(year, month - 1, day);\n return date.getFullYear() === year &&\n date.getMonth() === month - 1 &&\n date.getDate() === day\n ? date\n : undefined;\n};\n\nconst validateRetentionDays = (retentionDays: number): void => {\n if (!Number.isSafeInteger(retentionDays) || retentionDays < 0) {\n throw new Error(\"Log retention days must be a non-negative integer.\");\n }\n};\n\nexport interface PurgeRunLogsOptions {\n /** Host repository containing `.shipyard/logs/`. */\n readonly repoDir: string;\n /** Remove run logs strictly older than this many calendar days. Omit to remove every default run log. */\n readonly retentionDays?: number;\n /** Local time used to calculate the cutoff. Primarily useful for tests. */\n readonly now?: Date;\n}\n\nexport interface PurgeRunLogsResult {\n readonly logsDir: string;\n readonly retentionDays?: number;\n /** Dated directories on or after this local date are retained when age-based purging is used. */\n readonly cutoffDate?: string;\n readonly scannedDirectories: number;\n readonly removedCount: number;\n /** Directory names removed from `logsDir`, without the parent path. */\n readonly removedDirectories: readonly string[];\n /** Root-level log filenames removed from `logsDir`. */\n readonly removedFiles: readonly string[];\n}\n\n/**\n * Remove default run logs, either all of them or only those older than an\n * explicit retention threshold.\n *\n * Dated default run-log directories and regular root-level `.log` files inside\n * the managed logs directory are considered. Other files, malformed directory\n * names, and symlink entries are left alone.\n */\nexport const purgeRunLogs = async (\n options: PurgeRunLogsOptions,\n): Promise<PurgeRunLogsResult> => {\n const retentionDays = options.retentionDays;\n if (retentionDays !== undefined) validateRetentionDays(retentionDays);\n\n const now = options.now ?? new Date();\n if (retentionDays !== undefined && Number.isNaN(now.getTime())) {\n throw new Error(\"Cannot purge logs using an invalid date.\");\n }\n const cutoff =\n retentionDays === undefined\n ? undefined\n : new Date(\n now.getFullYear(),\n now.getMonth(),\n now.getDate() - retentionDays,\n );\n const cutoffDate =\n cutoff === undefined ? undefined : buildLogDirectoryName(cutoff);\n const logsDir = join(options.repoDir, CONFIG_DIR, LOGS_DIR);\n\n let entries;\n try {\n entries = await readdir(logsDir, { withFileTypes: true });\n } catch (error) {\n if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n return {\n logsDir,\n ...(retentionDays === undefined ? {} : { retentionDays, cutoffDate }),\n scannedDirectories: 0,\n removedCount: 0,\n removedDirectories: [],\n removedFiles: [],\n };\n }\n throw error;\n }\n\n const removedDirectories: string[] = [];\n const removedFiles: string[] = [];\n let scannedDirectories = 0;\n for (const entry of entries) {\n const entryPath = join(logsDir, entry.name);\n if (entry.isFile() && entry.name.endsWith(\".log\")) {\n const file = await lstat(entryPath);\n if (file.isFile() && (cutoff === undefined || file.mtime < cutoff)) {\n await rm(entryPath, { force: true });\n removedFiles.push(entry.name);\n }\n continue;\n }\n if (!entry.isDirectory()) continue;\n const directoryDate = parseLogDirectoryDate(entry.name);\n if (directoryDate === undefined) continue;\n scannedDirectories += 1;\n if (cutoff !== undefined && directoryDate >= cutoff) continue;\n\n await rm(entryPath, { recursive: true, force: true });\n removedDirectories.push(entry.name);\n }\n\n return {\n logsDir,\n ...(retentionDays === undefined ? {} : { retentionDays, cutoffDate }),\n scannedDirectories,\n removedCount: removedDirectories.length + removedFiles.length,\n removedDirectories,\n removedFiles,\n };\n};\n"]}
|