@smthrs/std 1.0.0-rc.7 → 1.0.0-rc.9
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/CHANGELOG.md +14 -0
- package/README.md +40 -36
- package/dist/cjs/Checkpoints.d.ts +37 -0
- package/dist/cjs/Checkpoints.d.ts.map +1 -1
- package/dist/cjs/Checkpoints.js +75 -0
- package/dist/cjs/Checkpoints.js.map +2 -2
- package/dist/cjs/PortableSearch.d.ts +13 -1
- package/dist/cjs/PortableSearch.d.ts.map +1 -1
- package/dist/cjs/PortableSearch.js +8 -1
- package/dist/cjs/PortableSearch.js.map +2 -2
- package/dist/cjs/RemoteSearch.d.ts +54 -0
- package/dist/cjs/RemoteSearch.d.ts.map +1 -0
- package/dist/cjs/RemoteSearch.js +134 -0
- package/dist/cjs/RemoteSearch.js.map +7 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/cjs/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/cjs/internal/EnvelopePrecheck.js +235 -36
- package/dist/cjs/internal/EnvelopePrecheck.js.map +3 -3
- package/dist/cjs/internal/Exec.d.ts +12 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +8 -4
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/cjs/internal/RemoteTree.d.ts +60 -0
- package/dist/cjs/internal/RemoteTree.d.ts.map +1 -0
- package/dist/cjs/internal/RemoteTree.js +123 -0
- package/dist/cjs/internal/RemoteTree.js.map +7 -0
- package/dist/cjs/internal/Tar.d.ts +23 -0
- package/dist/cjs/internal/Tar.d.ts.map +1 -0
- package/dist/cjs/internal/Tar.js +86 -0
- package/dist/cjs/internal/Tar.js.map +7 -0
- package/dist/esm/Checkpoints.d.ts +37 -0
- package/dist/esm/Checkpoints.d.ts.map +1 -1
- package/dist/esm/Checkpoints.js +85 -0
- package/dist/esm/Checkpoints.js.map +1 -1
- package/dist/esm/PortableSearch.d.ts +13 -1
- package/dist/esm/PortableSearch.d.ts.map +1 -1
- package/dist/esm/PortableSearch.js +16 -0
- package/dist/esm/PortableSearch.js.map +1 -1
- package/dist/esm/RemoteSearch.d.ts +54 -0
- package/dist/esm/RemoteSearch.d.ts.map +1 -0
- package/dist/esm/RemoteSearch.js +162 -0
- package/dist/esm/RemoteSearch.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/esm/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.js +321 -62
- package/dist/esm/internal/EnvelopePrecheck.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +12 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +30 -9
- package/dist/esm/internal/Exec.js.map +1 -1
- package/dist/esm/internal/RemoteTree.d.ts +60 -0
- package/dist/esm/internal/RemoteTree.d.ts.map +1 -0
- package/dist/esm/internal/RemoteTree.js +132 -0
- package/dist/esm/internal/RemoteTree.js.map +1 -0
- package/dist/esm/internal/Tar.d.ts +23 -0
- package/dist/esm/internal/Tar.d.ts.map +1 -0
- package/dist/esm/internal/Tar.js +97 -0
- package/dist/esm/internal/Tar.js.map +1 -0
- package/docs/README.md +3 -1
- package/docs/api.md +48 -21
- package/docs/concepts/effects-and-capabilities.md +3 -1
- package/docs/guides/bind-the-standard-flows.md +3 -1
- package/docs/guides/pin-a-checkpoint.md +20 -0
- package/docs/guides/run-a-shell-command.md +4 -1
- package/docs/installation.md +13 -9
- package/docs/quickstart.md +1 -1
- package/docs/troubleshooting.md +20 -0
- package/package.json +16 -6
- package/src/Checkpoints.ts +137 -0
- package/src/PortableSearch.ts +20 -0
- package/src/RemoteSearch.ts +199 -0
- package/src/index.ts +3 -0
- package/src/internal/EnvelopePrecheck.ts +320 -64
- package/src/internal/Exec.ts +32 -8
- package/src/internal/RemoteTree.ts +159 -0
- package/src/internal/Tar.ts +95 -0
package/docs/api.md
CHANGED
|
@@ -180,10 +180,14 @@ resolves its own services when it is built.
|
|
|
180
180
|
The in-process peer. Walks the injected `FileSystem`, so it needs no external
|
|
181
181
|
binary and is browser-safe.
|
|
182
182
|
|
|
183
|
-
| Export
|
|
184
|
-
|
|
|
185
|
-
| `make`
|
|
186
|
-
| `layer`
|
|
183
|
+
| Export | Type |
|
|
184
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| `make` | `(services: Context<FileSystem \| Path>) => Search.Search` |
|
|
186
|
+
| `layer` | `Layer<Search.Search, never, FileSystem \| Path>` |
|
|
187
|
+
| `searched` | `(input: Pick<GrepInput, "root" \| "hidden" \| "noIgnore" \| "globs">) => Effect<ReadonlyArray<string>, StdError, FileSystem \| Path>` |
|
|
188
|
+
|
|
189
|
+
`searched` names the files `grep` reads for an input, in the order it reads
|
|
190
|
+
them; `RemoteSearch` fetches exactly those.
|
|
187
191
|
|
|
188
192
|
## NativeSearch
|
|
189
193
|
|
|
@@ -201,6 +205,26 @@ start `rg` is `provider_unavailable`. The default layer uses the host environmen
|
|
|
201
205
|
allowlist. `make` accepts explicit environment declarations as its second
|
|
202
206
|
argument; those names override the allowlist.
|
|
203
207
|
|
|
208
|
+
## RemoteSearch
|
|
209
|
+
|
|
210
|
+
The peer for a tree on another machine, such as a container or a sandbox
|
|
211
|
+
machine. It lists the root with the machine's `find`, brings back its
|
|
212
|
+
`.gitignore` files and then the files the search reads with `tar`, and lets
|
|
213
|
+
`PortableSearch` answer over that copy, so every result, notice, and refusal is
|
|
214
|
+
the in-process peer's. Each search is two round trips through the machine's
|
|
215
|
+
spawner, whatever the tree's size.
|
|
216
|
+
|
|
217
|
+
| Export | Type | Meaning |
|
|
218
|
+
| ---------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
219
|
+
| `maxBytes` | `268_435_456` | The most file content one search brings back; past it, `response_too_large`. |
|
|
220
|
+
| `make` | `(services: Context<ChildProcessSpawner \| Path>, options?: { maxBytes?: number }) => Search.Search` | The spawner runs on the machine; `Path` resolves a relative root there. |
|
|
221
|
+
|
|
222
|
+
The machine needs `sh`, `find`, `xargs`, `tar`, `head`, and `base64`; GNU,
|
|
223
|
+
BusyBox, and BSD userlands all have them, and a machine without one refuses
|
|
224
|
+
with `command_failed` naming it. The program goes in on standard input and
|
|
225
|
+
the answer comes back as base64, so a transport whose argv is bounded or whose
|
|
226
|
+
output channel carries only text answers intact.
|
|
227
|
+
|
|
204
228
|
## SearchContract
|
|
205
229
|
|
|
206
230
|
The validation and matching rules both peers build on, exported so a third peer
|
|
@@ -307,23 +331,26 @@ How the project under test runs its suite, declared once by the host.
|
|
|
307
331
|
|
|
308
332
|
Pinned trees, and the scratch checkouts a call runs against.
|
|
309
333
|
|
|
310
|
-
| Export | Type
|
|
311
|
-
| ------------------ |
|
|
312
|
-
| `Checkpoints` | interface and `Context.Service` tag
|
|
313
|
-
| `Snapshot` | `Schema.Class`
|
|
314
|
-
| `Materialized` | interface
|
|
315
|
-
| `baseId` | `"base"`
|
|
316
|
-
| `scratchDirectory` | `".flows-checkpoints"`
|
|
317
|
-
| `configSection` | `"flows-checkpoint"`
|
|
318
|
-
| `make` | `(service: Checkpoints) => Checkpoints`
|
|
319
|
-
| `unavailable` | `(id: string) => StdError`
|
|
320
|
-
| `makeNoop` | `() => Checkpoints`
|
|
321
|
-
| `layerNoop` | `Layer<Checkpoints>`
|
|
322
|
-
| `GitOptions` | interface
|
|
323
|
-
| `makeGit` | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>`
|
|
324
|
-
| `layerGit` | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>`
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
334
|
+
| Export | Type | Meaning |
|
|
335
|
+
| ------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
336
|
+
| `Checkpoints` | interface and `Context.Service` tag | `{ capture, materialize }`. |
|
|
337
|
+
| `Snapshot` | `Schema.Class` | `{ id, ref }`, identified as `@smthrs/std/Checkpoints/Snapshot`. |
|
|
338
|
+
| `Materialized` | interface | `id`, `host`, `guest`, `root`, `guestRoot`. |
|
|
339
|
+
| `baseId` | `"base"` | The id naming the tree the run opened on. |
|
|
340
|
+
| `scratchDirectory` | `".flows-checkpoints"` | Where a checkpoint is materialized, relative to the root. |
|
|
341
|
+
| `configSection` | `"flows-checkpoint"` | The git-config section minted checkpoints are recorded under. |
|
|
342
|
+
| `make` | `(service: Checkpoints) => Checkpoints` | |
|
|
343
|
+
| `unavailable` | `(id: string) => StdError` | The refusal a host that pins nothing answers with. |
|
|
344
|
+
| `makeNoop` | `() => Checkpoints` | Fails both calls with `unavailable`. |
|
|
345
|
+
| `layerNoop` | `Layer<Checkpoints>` | |
|
|
346
|
+
| `GitOptions` | interface | `root`, `cwd?`, `baseRef?`. |
|
|
347
|
+
| `makeGit` | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>` | |
|
|
348
|
+
| `layerGit` | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>` | |
|
|
349
|
+
| `ContainerOptions` | interface | `container`, `cwd?`. |
|
|
350
|
+
| `makeContainer` | `(options: ContainerOptions) => Effect<Checkpoints, never, ChildProcessSpawner \| Container>` | Runs git inside the container `bash` runs in. |
|
|
351
|
+
| `layerContainer` | `(options: ContainerOptions) => Layer<Checkpoints, never, ChildProcessSpawner \| Container>` | |
|
|
352
|
+
| `Relocation` | tagged union | `Relocated`, `UnsupportedFlow`, `AbsolutePath`, `OutsideTree`. |
|
|
353
|
+
| `relocate` | `(flow: string, input: Json, materialized: Materialized) => Relocation` | Rewrites one call's input onto a checkpoint. |
|
|
327
354
|
|
|
328
355
|
`capture(id)` returns a `Snapshot`. `materialize(id, use)` is scoped: it hands
|
|
329
356
|
the tree to `use` and removes the checkout however that effect ends. Host git
|
|
@@ -112,7 +112,9 @@ handler checks the command against them before spawning anything. Read what the
|
|
|
112
112
|
check does, because the name promises more than it delivers.
|
|
113
113
|
|
|
114
114
|
The check is **lexical**. It tokenizes the command line, keeping quoted spans
|
|
115
|
-
whole, treats each physical line as its own command,
|
|
115
|
+
whole, treats each physical line as its own command, reads the body of a
|
|
116
|
+
command substitution, the argument of a shell's `-c` flag, and the arguments of
|
|
117
|
+
`eval` as commands of their own, classifies each path token
|
|
116
118
|
as a read or a write from the command that governs it, resolves both the token
|
|
117
119
|
and every declaration to a canonical absolute path, and refuses the call with
|
|
118
120
|
`outside_declared_reads` or `outside_declared_writes` when one falls outside.
|
|
@@ -36,7 +36,9 @@ const host = Layer.mergeAll(
|
|
|
36
36
|
|
|
37
37
|
Swap `NativeSearch.layer` for `PortableSearch.layer` to search in process with
|
|
38
38
|
no external binary. Both satisfy the same `Search` service, and nothing above
|
|
39
|
-
the layer changes.
|
|
39
|
+
the layer changes. When the files live on another machine, such as a container
|
|
40
|
+
or a sandbox machine, `RemoteSearch.make` over that machine's spawner and `Path`
|
|
41
|
+
answers in two round trips where the portable peer would ask once per file.
|
|
40
42
|
|
|
41
43
|
A host that cannot serve a service binds its refusal layer instead of leaving it
|
|
42
44
|
out, so the failure names the missing service rather than a missing context:
|
|
@@ -32,6 +32,26 @@ const store = Checkpoints.layerGit({
|
|
|
32
32
|
`Checkpoints.layerNoop`, whose refusal says to take the reading on the live tree
|
|
33
33
|
instead.
|
|
34
34
|
|
|
35
|
+
## Bind the container store
|
|
36
|
+
|
|
37
|
+
When the repository lives only in a container and `bash` runs there, bind
|
|
38
|
+
`layerContainer` instead. It runs git inside that container through the
|
|
39
|
+
`Container` transport `bash` uses, so `ctx.base` and `ctx.checkpoint()` read the
|
|
40
|
+
container's tree. A git store would look for the repository on the host and
|
|
41
|
+
refuse every call that names a checkpoint.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const store = Checkpoints.layerContainer({
|
|
45
|
+
container: "task", // the container `bash` runs in
|
|
46
|
+
cwd: "/app" // optional; absent is the container's working directory
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`layerContainer` requires a `ChildProcessSpawner` and a `Container`. It records
|
|
51
|
+
and names a checkpoint as the git store does, and checks a tree out at the same
|
|
52
|
+
`<top>/.flows-checkpoints/<id>-<lease>` path inside the container, so a
|
|
53
|
+
relocated `bash` call keeps its subdirectory.
|
|
54
|
+
|
|
35
55
|
## Capture and materialize
|
|
36
56
|
|
|
37
57
|
```ts
|
|
@@ -91,7 +91,10 @@ tracked in [#1790](https://github.com/smithersai/smithers/issues/1790).
|
|
|
91
91
|
A host sealed to one container is the exception. `SMITHERS_BASH_CONTAINER` names
|
|
92
92
|
the container, and `bash` and `test` then run in it through the `docker` CLI.
|
|
93
93
|
Every other container, and a call that names none, fails with
|
|
94
|
-
`outside_container`. The
|
|
94
|
+
`outside_container`. The file flows (`read`, `edit`, `apply_patch`, `grep` and
|
|
95
|
+
the rest) act on the container's files through the same transport, resolving a
|
|
96
|
+
relative path under `SMITHERS_TEST_CWD`, and `grep` and `glob` answer through
|
|
97
|
+
`RemoteSearch`. The transport process runs outside the OS sandbox, which
|
|
95
98
|
denies the daemon's socket, and receives the host's `DOCKER_HOST`,
|
|
96
99
|
`DOCKER_CONTEXT`, `DOCKER_CONFIG`, `DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH`,
|
|
97
100
|
so it reaches a rootless daemon.
|
package/docs/installation.md
CHANGED
|
@@ -7,7 +7,11 @@ sidebar:
|
|
|
7
7
|
|
|
8
8
|
## Install the package
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install @smthrs/std@next effect
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The `next` tag carries the 1.0 release candidates.
|
|
11
15
|
|
|
12
16
|
It requires
|
|
13
17
|
Node.js 26.4.0 or later and ships as both ESM and CommonJS with TypeScript
|
|
@@ -92,14 +96,14 @@ Six flows reach the host through a service rather than through a platform layer.
|
|
|
92
96
|
Each one has a refusal layer for a host that cannot serve it, so a flow whose
|
|
93
97
|
service is missing says so instead of succeeding quietly.
|
|
94
98
|
|
|
95
|
-
| Service | Refusal layer | Working layer
|
|
96
|
-
| ---------------- | -------------------------- |
|
|
97
|
-
| `Search` | `Search.layerNoop` | `PortableSearch.layer` or `NativeSearch.layer`
|
|
98
|
-
| `Container` | `Container.layerNoop` | `Container.layerCommand({ program: "docker" })`
|
|
99
|
-
| `TestRunner` | `TestRunner.layerNoop` | `TestRunner.layer(runner)`
|
|
100
|
-
| `Checkpoints` | `Checkpoints.layerNoop` | `Checkpoints.layerGit({ root })`
|
|
101
|
-
| `WebSearch` | `WebSearch.layerNoop` | `ExaWebSearch.layer(credentialId)`
|
|
102
|
-
| `LanguageServer` | `LanguageServer.layerNoop` | `NodeLanguageServer.layer(config)`
|
|
99
|
+
| Service | Refusal layer | Working layer |
|
|
100
|
+
| ---------------- | -------------------------- | ------------------------------------------------------------------------------- |
|
|
101
|
+
| `Search` | `Search.layerNoop` | `PortableSearch.layer` or `NativeSearch.layer` |
|
|
102
|
+
| `Container` | `Container.layerNoop` | `Container.layerCommand({ program: "docker" })` |
|
|
103
|
+
| `TestRunner` | `TestRunner.layerNoop` | `TestRunner.layer(runner)` |
|
|
104
|
+
| `Checkpoints` | `Checkpoints.layerNoop` | `Checkpoints.layerGit({ root })` or `Checkpoints.layerContainer({ container })` |
|
|
105
|
+
| `WebSearch` | `WebSearch.layerNoop` | `ExaWebSearch.layer(credentialId)` |
|
|
106
|
+
| `LanguageServer` | `LanguageServer.layerNoop` | `NodeLanguageServer.layer(config)` |
|
|
103
107
|
|
|
104
108
|
`NativeSearch.layer` drives the `rg` executable, so a host that binds it needs
|
|
105
109
|
ripgrep on the path. `PortableSearch.layer` needs nothing beyond `FileSystem`
|
package/docs/quickstart.md
CHANGED
|
@@ -14,7 +14,7 @@ without an external binary and without an API key.
|
|
|
14
14
|
## Prerequisites
|
|
15
15
|
|
|
16
16
|
- Node.js 26.4.0 or later.
|
|
17
|
-
- A
|
|
17
|
+
- A project with the packages installed: `npm install @smthrs/std@next @effect/platform-node effect`.
|
|
18
18
|
|
|
19
19
|
## Make a tree to search
|
|
20
20
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -149,6 +149,14 @@ not covered by its own `reads` or `writes`. The message names the resolved path.
|
|
|
149
149
|
check is lexical and fail-closed: it reads the tokens in the command line, not
|
|
150
150
|
what the process will actually touch.
|
|
151
151
|
|
|
152
|
+
## invalid_input: a command nested deeper than 32 levels
|
|
153
|
+
|
|
154
|
+
**What happened.** A hermetic `bash` call nested command substitutions, quotes,
|
|
155
|
+
shell `-c` arguments, or `eval` calls more than 32 levels deep. The pre-check
|
|
156
|
+
refuses text it cannot read in bounded time rather than run it unchecked.
|
|
157
|
+
|
|
158
|
+
**What to change.** Flatten the command, or use `mode: "unhermetic"`.
|
|
159
|
+
|
|
152
160
|
## invalid_input: a hermetic call that names a container
|
|
153
161
|
|
|
154
162
|
**What happened.** `mode: "hermetic"` together with `container`. A container has
|
|
@@ -160,6 +168,18 @@ The same rule refuses a hermetic `script` whose `interpreter` is not a shell:
|
|
|
160
168
|
the pre-check reads shell text, and Python is not shell text. Run it
|
|
161
169
|
unhermetically, or express it as shell.
|
|
162
170
|
|
|
171
|
+
## invalid_input: Command not started: its arguments exceed the operating system's size limit (E2BIG)
|
|
172
|
+
|
|
173
|
+
**What happened.** The operating system refused to start the command because
|
|
174
|
+
its arguments are too large: one argument over 128 KiB on Linux, or all of them
|
|
175
|
+
over 1 MiB on macOS. A long heredoc in `command`, or a file's text passed in
|
|
176
|
+
`args`, is one such argument. For a `container` call the host's `docker exec`
|
|
177
|
+
carries the same arguments, so the same limit applies.
|
|
178
|
+
|
|
179
|
+
**What to change.** Send the text on standard input, which has no size limit.
|
|
180
|
+
Put a program and its data in `script`, or keep `command` short and pass the
|
|
181
|
+
data in `stdin`. Never put long text in `command` or `args`.
|
|
182
|
+
|
|
163
183
|
## provider_unavailable: this host has no container transport
|
|
164
184
|
|
|
165
185
|
**What happened.** A call named a `container` and no `Container` service was
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/std",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.9",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "agent"
|
|
@@ -252,6 +252,16 @@
|
|
|
252
252
|
"default": "./dist/cjs/Relocate.js"
|
|
253
253
|
}
|
|
254
254
|
},
|
|
255
|
+
"./RemoteSearch": {
|
|
256
|
+
"import": {
|
|
257
|
+
"types": "./dist/esm/RemoteSearch.d.ts",
|
|
258
|
+
"default": "./dist/esm/RemoteSearch.js"
|
|
259
|
+
},
|
|
260
|
+
"require": {
|
|
261
|
+
"types": "./dist/cjs/RemoteSearch.d.ts",
|
|
262
|
+
"default": "./dist/cjs/RemoteSearch.js"
|
|
263
|
+
}
|
|
264
|
+
},
|
|
255
265
|
"./Search": {
|
|
256
266
|
"import": {
|
|
257
267
|
"types": "./dist/esm/Search.d.ts",
|
|
@@ -401,11 +411,11 @@
|
|
|
401
411
|
"tag": "next"
|
|
402
412
|
},
|
|
403
413
|
"dependencies": {
|
|
404
|
-
"@smthrs/capability": "1.0.0-rc.
|
|
405
|
-
"@smthrs/control": "1.0.0-rc.
|
|
406
|
-
"@smthrs/core": "1.0.0-rc.
|
|
407
|
-
"@smthrs/kernel": "1.0.0-rc.
|
|
408
|
-
"@smthrs/model": "1.0.0-rc.
|
|
414
|
+
"@smthrs/capability": "1.0.0-rc.9",
|
|
415
|
+
"@smthrs/control": "1.0.0-rc.9",
|
|
416
|
+
"@smthrs/core": "1.0.0-rc.9",
|
|
417
|
+
"@smthrs/kernel": "1.0.0-rc.9",
|
|
418
|
+
"@smthrs/model": "1.0.0-rc.9"
|
|
409
419
|
},
|
|
410
420
|
"devDependencies": {
|
|
411
421
|
"@effect/language-service": "0.87.2",
|
package/src/Checkpoints.ts
CHANGED
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
|
|
73
73
|
import { ChildProcessSpawner } from "@smthrs/kernel/ChildProcessSpawner"
|
|
74
74
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
75
|
+
import * as Container from "./Container.ts"
|
|
76
|
+
import * as Exec from "./internal/Exec.ts"
|
|
75
77
|
import { GitWorktree } from "./internal/GitWorktree.ts"
|
|
76
78
|
import { withoutTrailingSlash } from "./internal/Paths.ts"
|
|
77
79
|
import * as Prune from "./internal/Prune.ts"
|
|
@@ -409,6 +411,141 @@ const gitStore = (options: GitOptions, spawner: ChildProcessSpawner["Service"]):
|
|
|
409
411
|
*/
|
|
410
412
|
export const layerGit = (options: GitOptions): Layer.Layer<Checkpoints, never, ChildProcessSpawner> =>
|
|
411
413
|
Layer.effect(Checkpoints)(makeGit(options))
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* What {@link makeContainer} needs to know about the container it pins in.
|
|
417
|
+
*
|
|
418
|
+
* @category models
|
|
419
|
+
* @since 1.0.0-rc.8
|
|
420
|
+
*/
|
|
421
|
+
export interface ContainerOptions {
|
|
422
|
+
/** The container `bash` runs in. */
|
|
423
|
+
readonly container: string
|
|
424
|
+
/** A directory of the repository inside it; absent is the container's own working directory. */
|
|
425
|
+
readonly cwd?: string | undefined
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `$1` is the id. Records the working tree with `stash create` over a copy of
|
|
430
|
+
* the index, names the commit in config as {@link makeGit} does, and prints it.
|
|
431
|
+
*/
|
|
432
|
+
const pinScript = [
|
|
433
|
+
"set -e",
|
|
434
|
+
"cd \"$(git rev-parse --show-toplevel)\"",
|
|
435
|
+
"copy=$(mktemp -d)",
|
|
436
|
+
"trap 'rm -rf -- \"$copy\"' EXIT",
|
|
437
|
+
"index=$(git rev-parse --git-path index)",
|
|
438
|
+
"if [ -e \"$index\" ]; then cp -p -- \"$index\" \"$copy/index\"; fi",
|
|
439
|
+
"commit=$(GIT_INDEX_FILE=\"$copy/index\" git -c user.name=flows -c user.email=flows@localhost stash create \"flows checkpoint $1\")",
|
|
440
|
+
"[ -n \"$commit\" ] || commit=$(git rev-parse --verify 'HEAD^{commit}')",
|
|
441
|
+
`git config "${configSection}.$1" "$commit"`,
|
|
442
|
+
"printf '%s\\n' \"$commit\""
|
|
443
|
+
].join("\n")
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* `$1` is the id, `$2` the lease. Checks the tree out at
|
|
447
|
+
* `<top>/.flows-checkpoints/<id>-<lease>` in a repository of its own whose
|
|
448
|
+
* objects are the workspace's, and prints the top and the checkout.
|
|
449
|
+
*/
|
|
450
|
+
const checkoutScript = [
|
|
451
|
+
"set -e",
|
|
452
|
+
"top=$(git rev-parse --show-toplevel)",
|
|
453
|
+
"cd \"$top\"",
|
|
454
|
+
"common=$(cd \"$(git rev-parse --git-common-dir)\" && pwd -P)",
|
|
455
|
+
`if [ "$1" = ${baseId} ]; then`,
|
|
456
|
+
` commit=$(git rev-parse --verify --quiet '${TestRunner.captureBase}^{commit}' || git rev-parse --verify --quiet 'HEAD^{commit}') || { echo "No commit resolves from ${TestRunner.captureBase} or HEAD in $top." >&2; exit 1; }`,
|
|
457
|
+
"else",
|
|
458
|
+
` commit=$(git config --get "${configSection}.$1") || { echo "No checkpoint is stored under $1 in $top. Take the reading on the live tree instead." >&2; exit 1; }`,
|
|
459
|
+
"fi",
|
|
460
|
+
`dir="$top/${scratchDirectory}/$1-$2"`,
|
|
461
|
+
"trap 'rm -rf -- \"$dir\"' EXIT",
|
|
462
|
+
`mkdir -p -- "$top/${scratchDirectory}"`,
|
|
463
|
+
"git init --quiet \"$dir\"",
|
|
464
|
+
"printf '%s/objects\\n' \"$common\" > \"$dir/.git/objects/info/alternates\"",
|
|
465
|
+
"git -C \"$dir\" checkout --quiet --detach \"$commit\"",
|
|
466
|
+
"trap - EXIT",
|
|
467
|
+
"printf '%s\\n%s\\n' \"$top\" \"$dir\""
|
|
468
|
+
].join("\n")
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Builds the store for a host whose `bash` runs in one container.
|
|
472
|
+
*
|
|
473
|
+
* The repository is the container's, so every git command runs there, through
|
|
474
|
+
* the same {@link Container.Container} transport `bash` takes. The host store
|
|
475
|
+
* looked in the CLI's own directory instead and refused every `at` in 112 of
|
|
476
|
+
* 123 trials of the 2026-10-14 SWE-bench Pro run. This keeps that store's
|
|
477
|
+
* shape: a checkpoint is a `stash create` commit named in config, and a tree
|
|
478
|
+
* is checked out under the repository for one call, so a relocated `bash`
|
|
479
|
+
* call keeps its subdirectory and still finds what the workspace installs
|
|
480
|
+
* beside it. That git may run what the repository configures, but only inside
|
|
481
|
+
* the container the agent already commands.
|
|
482
|
+
*
|
|
483
|
+
* @category constructors
|
|
484
|
+
* @since 1.0.0-rc.8
|
|
485
|
+
*/
|
|
486
|
+
export const makeContainer = (
|
|
487
|
+
options: ContainerOptions
|
|
488
|
+
): Effect.Effect<Checkpoints, never, ChildProcessSpawner | Container.Container> =>
|
|
489
|
+
Effect.gen(function*() {
|
|
490
|
+
const spawner = yield* ChildProcessSpawner
|
|
491
|
+
const transport = yield* Container.Container
|
|
492
|
+
const sh = (script: string, args: ReadonlyArray<string>) =>
|
|
493
|
+
transport.exec({
|
|
494
|
+
container: options.container,
|
|
495
|
+
file: "sh",
|
|
496
|
+
args: ["-c", script, "sh", ...args],
|
|
497
|
+
cwd: options.cwd,
|
|
498
|
+
stdin: false
|
|
499
|
+
}).pipe(
|
|
500
|
+
Effect.flatMap((plan) =>
|
|
501
|
+
Exec.exec(plan.file, { args: plan.args, env: plan.env }).pipe(
|
|
502
|
+
Effect.mapError((error) => Exec.toStdError(plan.file, error))
|
|
503
|
+
)
|
|
504
|
+
),
|
|
505
|
+
Effect.flatMap((result) =>
|
|
506
|
+
result.exitCode === 0
|
|
507
|
+
? Effect.succeed(result.stdout.trim().split("\n"))
|
|
508
|
+
: Effect.fail(failed(`git in container ${options.container} failed: ${result.stderr.trim()}`))
|
|
509
|
+
),
|
|
510
|
+
Effect.provideService(ChildProcessSpawner, spawner)
|
|
511
|
+
)
|
|
512
|
+
const named = (id: string) =>
|
|
513
|
+
namable.test(id)
|
|
514
|
+
? Effect.void
|
|
515
|
+
: Effect.fail(failed(`A checkpoint id must match ${namable.source}; ${id} does not.`, "invalid_input"))
|
|
516
|
+
return make({
|
|
517
|
+
capture: (id) =>
|
|
518
|
+
named(id).pipe(
|
|
519
|
+
Effect.andThen(sh(pinScript, [id])),
|
|
520
|
+
Effect.map(([commit]) => new Snapshot({ id, ref: commit! }))
|
|
521
|
+
),
|
|
522
|
+
materialize: (id, use) =>
|
|
523
|
+
named(id).pipe(Effect.andThen(Effect.acquireUseRelease(
|
|
524
|
+
sh(checkoutScript, [id, globalThis.crypto.randomUUID()]),
|
|
525
|
+
([root, checkout]) => use({ id, host: checkout!, guest: checkout!, root: root!, guestRoot: root! }),
|
|
526
|
+
([, checkout]) =>
|
|
527
|
+
sh("rm -rf -- \"$1\"", [checkout!]).pipe(
|
|
528
|
+
Effect.catch((error) =>
|
|
529
|
+
Effect.logWarning("could not remove a scratch checkout").pipe(
|
|
530
|
+
Effect.annotateLogs({ path: checkout, reason: error.message })
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
)))
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Provides {@link makeContainer}.
|
|
540
|
+
*
|
|
541
|
+
* @category layers
|
|
542
|
+
* @since 1.0.0-rc.8
|
|
543
|
+
*/
|
|
544
|
+
export const layerContainer = (
|
|
545
|
+
options: ContainerOptions
|
|
546
|
+
): Layer.Layer<Checkpoints, never, ChildProcessSpawner | Container.Container> =>
|
|
547
|
+
Layer.effect(Checkpoints)(makeContainer(options))
|
|
548
|
+
|
|
412
549
|
/**
|
|
413
550
|
* Rewrites one call's input so the call runs against a materialized checkpoint.
|
|
414
551
|
*
|
package/src/PortableSearch.ts
CHANGED
|
@@ -240,6 +240,26 @@ const glob = (
|
|
|
240
240
|
}
|
|
241
241
|
})
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The files this peer's `grep` reads for `input`, in the order it reads
|
|
245
|
+
* them: the walk under `input.root` with its `.gitignore` rules and
|
|
246
|
+
* `input.globs` applied and symbolic links left out, or the root itself when
|
|
247
|
+
* it names one file. A peer that has to bring a tree's bytes to this process
|
|
248
|
+
* before it can search them (`RemoteSearch`) fetches exactly these.
|
|
249
|
+
*
|
|
250
|
+
* @category queries
|
|
251
|
+
* @since 1.0.0-rc.8
|
|
252
|
+
*/
|
|
253
|
+
export const searched = (
|
|
254
|
+
input: Pick<Search.GrepInput, "root" | "hidden" | "noIgnore" | "globs">
|
|
255
|
+
): Effect.Effect<ReadonlyArray<string>, StdError.StdError, FileSystem.FileSystem | Path.Path> =>
|
|
256
|
+
Effect.gen(function*() {
|
|
257
|
+
const fileSystem = yield* FileSystem.FileSystem
|
|
258
|
+
const path = yield* Path.Path
|
|
259
|
+
const walked = yield* Walk.files(fileSystem, path, input.root, input.hidden, input.noIgnore)
|
|
260
|
+
return yield* candidates(fileSystem, path, walked, input.root, input.globs)
|
|
261
|
+
})
|
|
262
|
+
|
|
243
263
|
/**
|
|
244
264
|
* Captures filesystem and path services in the portable peer.
|
|
245
265
|
*
|