opencode-effect-enforcer 0.2.6 → 0.3.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 +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +15 -305
- package/guidance/progressive-disclosure-guidance.md +18 -24
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +2 -2
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +4 -4
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
- package/src/guidance.ts +0 -1
|
@@ -15,6 +15,9 @@ the yielded service and `get` / `contextEffect` / `contextEffectOption`: an entr
|
|
|
15
15
|
can fail when reacquired after successful preloading. Invalidating an active
|
|
16
16
|
RcMap/LayerMap entry releases it after its last borrower closes; a replacement
|
|
17
17
|
entry remains independently owned.
|
|
18
|
+
Preload keys with zero `idleTimeToLive` are skipped, including the default TTL.
|
|
19
|
+
Set a non-zero TTL when construction must eagerly acquire and validate them;
|
|
20
|
+
otherwise acquisition failures surface on first use.
|
|
18
21
|
|
|
19
22
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
20
23
|
Browse and read files there directly to look up APIs, types, and implementations.
|
|
@@ -47,7 +50,7 @@ interface Cache<Key, A, E = never, R = never> {
|
|
|
47
50
|
How to think about it:
|
|
48
51
|
|
|
49
52
|
- **Entries store the lookup `Exit`** — successes *and failures* are cached. A failed lookup keeps failing from cache until the entry expires, is invalidated, or is refreshed.
|
|
50
|
-
- **Concurrent `get`s of the same missing key share one lookup.**
|
|
53
|
+
- **Concurrent `get`s of the same missing key share one lookup fiber.** Each caller waits independently; interrupting one waiter does not cancel work still needed by another. The last departing waiter interrupts a pending lookup.
|
|
51
54
|
- **Insertion-ordered map = LRU.** Reads move the entry to the back; when capacity is exceeded the oldest entries are evicted.
|
|
52
55
|
- **TTL is computed per entry** from the lookup `Exit` and the key, against the fiber's `Clock` — `TestClock` works. Expiry is lazy: entries are removed when next touched.
|
|
53
56
|
- **`ScopedCache`** is the same model where each entry additionally owns a `Scope`: resources acquired during the lookup live exactly as long as the entry is cached.
|
|
@@ -191,7 +194,7 @@ If an older in-flight lookup is interrupted after `set` installs a newer value,
|
|
|
191
194
|
yield* Cache.invalidate(cache, 'k'); // remove one key; no-op if absent
|
|
192
195
|
yield* Cache.invalidateAll(cache); // clear everything
|
|
193
196
|
|
|
194
|
-
// Conditional: removes
|
|
197
|
+
// Conditional: awaits a pending entry, then removes a successful value matching the predicate.
|
|
195
198
|
// Returns false for missing, expired, failed, or non-matching entries.
|
|
196
199
|
const removed: boolean = yield* Cache.invalidateWhen(cache, 'k', (v) => v.stale);
|
|
197
200
|
```
|
|
@@ -205,7 +208,7 @@ const fresh = yield* Cache.refresh(cache, 'k');
|
|
|
205
208
|
- Always invokes the lookup, even for an unexpired entry, and resets the TTL from the new exit.
|
|
206
209
|
- For an **existing** key, the old entry keeps serving `get` callers until the new lookup completes — built-in stale-while-revalidate.
|
|
207
210
|
- For a **missing** key, a pending entry is inserted immediately; concurrent `get`s wait on it.
|
|
208
|
-
-
|
|
211
|
+
- `Cache.refresh` calls are **not deduplicated** — each runs the lookup independently. `ScopedCache.refresh` of a missing key delegates to `get` and shares that pending lookup; refreshes of existing keys run independently.
|
|
209
212
|
|
|
210
213
|
---
|
|
211
214
|
|
|
@@ -234,6 +237,11 @@ yield* Cache.get(cache, 'd'); // evicts 'b'
|
|
|
234
237
|
- `Duration.infinity` (the default) means no expiry. `Duration.zero` means the entry expires immediately — effectively "do not cache this result".
|
|
235
238
|
- `refresh` and re-`get`-after-expiry both restart the TTL clock; plain `get` hits do not extend it (no sliding expiration).
|
|
236
239
|
|
|
240
|
+
`Cache.get` does not retain a zero-TTL result after completion. A synchronous
|
|
241
|
+
zero-TTL lookup does not occupy capacity and evict a live entry; a pending lookup
|
|
242
|
+
still occupies capacity while it is shared. `ScopedCache` has the distinct lazy
|
|
243
|
+
resource-release behavior described in section 8.
|
|
244
|
+
|
|
237
245
|
Per-key and per-result TTL via `makeWith`:
|
|
238
246
|
|
|
239
247
|
```ts
|
|
@@ -274,7 +282,7 @@ const robust = yield* Cache.makeWith(fetchUser, {
|
|
|
274
282
|
});
|
|
275
283
|
```
|
|
276
284
|
|
|
277
|
-
**Interruption
|
|
285
|
+
**Interruption is not cached.** Missing-key lookups run in daemon fibers, shared by their waiters. One caller's interruption leaves the lookup alive while another caller is waiting. When the last waiter leaves before completion, the lookup is interrupted and its entry removed; a later `get` starts again. `ScopedCache` also closes that lookup's entry scope. Children forked with `forkChild` inside the lookup end with the lookup fiber, not with the requesting caller; use the entry scope for resource-lifetime background work.
|
|
278
286
|
|
|
279
287
|
`getSuccess`, `values`, and `entries` skip failed entries; `getOption` and `get` propagate the cached error; `invalidateWhen` returns `false` for failed entries (the predicate only sees successes).
|
|
280
288
|
|
|
@@ -341,6 +349,10 @@ The entry's scope is closed — releasing everything the lookup acquired — whe
|
|
|
341
349
|
4. **replaced** (`set` over an existing key; `refresh` closes the old entry's scope after the new lookup completes),
|
|
342
350
|
5. **orphaned by cache close** (the owning scope closes).
|
|
343
351
|
|
|
352
|
+
A pending shared lookup also closes its entry scope when its last waiter leaves.
|
|
353
|
+
Interrupting an existing-key `refresh` closes the replacement scope and leaves
|
|
354
|
+
the previous entry intact.
|
|
355
|
+
|
|
344
356
|
```ts
|
|
345
357
|
const tracker: Array<string> = [];
|
|
346
358
|
const cache = yield* ScopedCache.make({
|
|
@@ -434,7 +446,7 @@ const cacheB = yield* Cache.make({
|
|
|
434
446
|
const row = yield* Cache.get(cacheB, 1).pipe(Effect.provideService(Db, dbImpl));
|
|
435
447
|
```
|
|
436
448
|
|
|
437
|
-
At runtime the lookup
|
|
449
|
+
At runtime the lookup sees the construction-time context merged with the initiating caller's context (the caller's services win on conflicts). The shared lookup fiber inherits that caller's tracing context. Remember the lookup runs **once per miss** — later waiters do not replace the context for a pending entry.
|
|
438
450
|
|
|
439
451
|
The same option exists on `ScopedCache.make`/`makeWith`.
|
|
440
452
|
|
|
@@ -597,9 +609,9 @@ it.effect('expires entries after the TTL', () =>
|
|
|
597
609
|
2. **v4 renames type parameters, it does not reorder them** — v3's `Cache<Key, Value, Error>` becomes `Cache<Key, A, E, R>`: same value-before-error order with a services parameter appended. Only the pre-v3 `@effect/io` era used `Cache<Key, Error, Value>`.
|
|
598
610
|
3. **`Cache.makeWith(lookup, options)` vs `ScopedCache.makeWith({ lookup, ...options })`** — `Cache.makeWith` takes the lookup as a separate first argument; `ScopedCache.makeWith` (and both `make`s) take one options object containing `lookup`.
|
|
599
611
|
4. **Failures are cached with the default infinite TTL** — one transient lookup error fails that key forever. Use `makeWith` with an exit-aware TTL (`Exit.isSuccess(exit) ? ttl : Duration.zero`).
|
|
600
|
-
5. **
|
|
612
|
+
5. **Confusing caller cancellation with lookup cancellation** — a shared missing-key lookup survives while any waiter remains. The last departing waiter interrupts pending work; interrupted entries are removed, and `ScopedCache` closes their scopes. Failure TTL policy is still needed for ordinary failures.
|
|
601
613
|
6. **`timeToLive` in `make` is a `Duration.Input` value, not a function** — the `(exit, key) => Duration.Input` form only exists on `makeWith`; passing a function to `make` won't compile.
|
|
602
|
-
7.
|
|
614
|
+
7. **Assuming all refreshes deduplicate** — `Cache.refresh` and existing-key `ScopedCache.refresh` run independently. Only missing-key `ScopedCache.refresh` delegates to the shared `get` path. Serialize expensive refreshes if needed.
|
|
603
615
|
8. **Don't mutate key objects after first use** — plain objects compare structurally in v4 (equal-content literals do hit), but `Equal`/`Hash` cache their results per object, so a mutated key misbehaves silently. Prefer primitives or immutable `Data.Class`/`Schema.Class` keys.
|
|
604
616
|
9. **Don't `Effect.acquireRelease` inside a plain `Cache` lookup** — `Scope` leaks into `R` and finalizers attach to whatever outer scope is around, not to the entry: nothing is released on eviction/invalidation. Use `ScopedCache`, which provides a per-entry scope.
|
|
605
617
|
10. **ScopedCache values die with their entry** — after `invalidate`/eviction/expiry-purge the value's finalizers have run. Don't hold the value beyond the entry's lifetime; use `effect/Pool` for checkout semantics.
|
|
@@ -20,8 +20,11 @@ scalars containing colon-whitespace or a trailing colon: quote those values.
|
|
|
20
20
|
|
|
21
21
|
## Import Pattern
|
|
22
22
|
|
|
23
|
+
The CLI APIs are marked `@stability unstable` even at the stable package release.
|
|
24
|
+
Inspect `effect@4.0.0:packages/effect/src/cli/` for this baseline's contracts.
|
|
25
|
+
|
|
23
26
|
```typescript
|
|
24
|
-
import { Argument, Command, Flag, Prompt } from 'effect/
|
|
27
|
+
import { Argument, Command, Flag, Prompt } from 'effect/cli';
|
|
25
28
|
```
|
|
26
29
|
|
|
27
30
|
Platform services and runtime for the entry point:
|
|
@@ -37,7 +40,7 @@ Positional arguments are parsed in order. Use `Flag.Boolean` for toggles or `Arg
|
|
|
37
40
|
### Constructors
|
|
38
41
|
|
|
39
42
|
```typescript
|
|
40
|
-
import { Argument } from 'effect/
|
|
43
|
+
import { Argument } from 'effect/cli';
|
|
41
44
|
|
|
42
45
|
Argument.String('name'); // string
|
|
43
46
|
Argument.Int('count'); // number (integer)
|
|
@@ -64,7 +67,7 @@ Argument.FileSchema('config', MySchema); // reads and validates file via Schema
|
|
|
64
67
|
### Combinators
|
|
65
68
|
|
|
66
69
|
```typescript
|
|
67
|
-
import { Argument } from 'effect/
|
|
70
|
+
import { Argument } from 'effect/cli';
|
|
68
71
|
|
|
69
72
|
// Description for help text
|
|
70
73
|
Argument.String('file').pipe(Argument.withDescription('Input file'));
|
|
@@ -121,10 +124,16 @@ Argument.Int('count').pipe(
|
|
|
121
124
|
|
|
122
125
|
Flags are named options with `--name` or `-alias` syntax.
|
|
123
126
|
|
|
127
|
+
Negative numeric tokens such as `-3`, `-3.70`, `-.5`, and `-1e-3` are values,
|
|
128
|
+
not short-option bundles: `--lon -3.70` works with `Flag.Finite('lon')`.
|
|
129
|
+
Use `--` to pass trailing operands that otherwise look like options; a lone
|
|
130
|
+
`-` is also retained as a value. Numeric lexing does not replace the chosen
|
|
131
|
+
primitive's validation.
|
|
132
|
+
|
|
124
133
|
### Constructors
|
|
125
134
|
|
|
126
135
|
```typescript
|
|
127
|
-
import { Flag } from 'effect/
|
|
136
|
+
import { Flag } from 'effect/cli';
|
|
128
137
|
|
|
129
138
|
Flag.Boolean('verbose'); // required: --verbose / --no-verbose; omission fails
|
|
130
139
|
Flag.String('config'); // --config value
|
|
@@ -152,7 +161,7 @@ Flag.KeyValuePair('env'); // --env FOO=bar → Record<string, string>
|
|
|
152
161
|
### Combinators
|
|
153
162
|
|
|
154
163
|
```typescript
|
|
155
|
-
import { Flag } from 'effect/
|
|
164
|
+
import { Flag } from 'effect/cli';
|
|
156
165
|
|
|
157
166
|
// Alias
|
|
158
167
|
Flag.Boolean('verbose').pipe(
|
|
@@ -217,7 +226,7 @@ Bare boolean flags are required. `--verbose` produces `true`, `--no-verbose` pro
|
|
|
217
226
|
<!-- typecheck -->
|
|
218
227
|
```typescript
|
|
219
228
|
import { Effect } from 'effect';
|
|
220
|
-
import { Prompt } from 'effect/
|
|
229
|
+
import { Prompt } from 'effect/cli';
|
|
221
230
|
|
|
222
231
|
Prompt.Int({ message: 'Count', default: 42 });
|
|
223
232
|
Prompt.File({ message: 'Pick file', default: '/workspace/config.json' });
|
|
@@ -253,7 +262,7 @@ choice values rather than pre-escaping your domain values.
|
|
|
253
262
|
|
|
254
263
|
```typescript
|
|
255
264
|
import { Console, Effect } from 'effect';
|
|
256
|
-
import { Argument, Command, Flag } from 'effect/
|
|
265
|
+
import { Argument, Command, Flag } from 'effect/cli';
|
|
257
266
|
|
|
258
267
|
// Simple command (no config, no handler)
|
|
259
268
|
const version = Command.make('version');
|
|
@@ -509,7 +518,7 @@ Command.provideSync(MyService, (config) => makeMyService(config.env));
|
|
|
509
518
|
```typescript
|
|
510
519
|
import { NodeRuntime, NodeServices } from '@effect/platform-node';
|
|
511
520
|
import { Effect } from 'effect';
|
|
512
|
-
import { Command, Flag } from 'effect/
|
|
521
|
+
import { Command, Flag } from 'effect/cli';
|
|
513
522
|
|
|
514
523
|
const myCommand = Command.make(
|
|
515
524
|
'myapp',
|
|
@@ -5,13 +5,16 @@ description: Spawn and manage child processes using Effect's ChildProcess module
|
|
|
5
5
|
|
|
6
6
|
# Child Process Execution with Effect v4
|
|
7
7
|
|
|
8
|
+
Targets **Effect 4.0.0**, checked against the `effect@4.0.0` source tag. Keep platform packages on the same version. The process API is marked `@stability unstable`, so check contracts again before a minor-version upgrade.
|
|
9
|
+
|
|
8
10
|
## Overview
|
|
9
11
|
|
|
10
|
-
Node process-group cleanup waits for the leader and descendants. Without
|
|
12
|
+
On POSIX, Node process-group cleanup waits for the leader and descendants. Without
|
|
11
13
|
`forceKillAfter`, cleanup waits up to one second without escalating. With it,
|
|
12
14
|
the group receives SIGKILL at the deadline, then a final bounded wait. Native
|
|
13
15
|
timers drive escalation even under TestClock. `exitCode` / `isRunning` describe
|
|
14
16
|
the leader, not all descendants; do not use virtual time alone to prove OS cleanup.
|
|
17
|
+
On Windows, Node uses `taskkill` for tree termination and waits for the leader.
|
|
15
18
|
|
|
16
19
|
The `ChildProcess` module provides type-safe, composable process execution with automatic resource cleanup via `Scope`. Commands are AST values — built first with `make` and `pipeTo`, then executed via the `ChildProcessSpawner` service.
|
|
17
20
|
|
|
@@ -23,12 +26,12 @@ The `ChildProcess` module provides type-safe, composable process execution with
|
|
|
23
26
|
- Streaming output from long-running processes
|
|
24
27
|
- Managing process lifecycles with scoped cleanup
|
|
25
28
|
|
|
26
|
-
**Note:** This skill covers child process execution
|
|
29
|
+
**Note:** This skill covers child process execution; use `effect/cli` for building CLI applications.
|
|
27
30
|
|
|
28
31
|
## Import Pattern
|
|
29
32
|
|
|
30
33
|
```typescript
|
|
31
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
34
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
32
35
|
```
|
|
33
36
|
|
|
34
37
|
Platform layer (Node.js):
|
|
@@ -41,8 +44,9 @@ import { NodeServices } from '@effect/platform-node';
|
|
|
41
44
|
|
|
42
45
|
### Template Literal Form
|
|
43
46
|
|
|
47
|
+
<!-- typecheck -->
|
|
44
48
|
```typescript
|
|
45
|
-
import { ChildProcess } from 'effect/
|
|
49
|
+
import { ChildProcess } from 'effect/process';
|
|
46
50
|
|
|
47
51
|
// Simple command — parsed by whitespace
|
|
48
52
|
const cmd = ChildProcess.make`echo hello world`;
|
|
@@ -58,8 +62,9 @@ const cmd3 = ChildProcess.make`oxfmt ${files}`;
|
|
|
58
62
|
|
|
59
63
|
### Options + Template Literal Form
|
|
60
64
|
|
|
65
|
+
<!-- typecheck -->
|
|
61
66
|
```typescript
|
|
62
|
-
import { ChildProcess } from 'effect/
|
|
67
|
+
import { ChildProcess } from 'effect/process';
|
|
63
68
|
|
|
64
69
|
// Options object returns a tagged template function
|
|
65
70
|
const cmd = ChildProcess.make({ cwd: '/tmp' })`ls -la`;
|
|
@@ -73,8 +78,9 @@ const cmd2 = ChildProcess.make({
|
|
|
73
78
|
|
|
74
79
|
### Array Form
|
|
75
80
|
|
|
81
|
+
<!-- typecheck -->
|
|
76
82
|
```typescript
|
|
77
|
-
import { ChildProcess } from 'effect/
|
|
83
|
+
import { ChildProcess } from 'effect/process';
|
|
78
84
|
|
|
79
85
|
// Explicit command + args array
|
|
80
86
|
const cmd = ChildProcess.make('git', ['status'], {
|
|
@@ -99,8 +105,9 @@ const cmd3 = ChildProcess.make('node', {
|
|
|
99
105
|
|
|
100
106
|
### Command Options
|
|
101
107
|
|
|
108
|
+
<!-- typecheck -->
|
|
102
109
|
```typescript
|
|
103
|
-
import { ChildProcess } from 'effect/
|
|
110
|
+
import { ChildProcess } from 'effect/process';
|
|
104
111
|
|
|
105
112
|
const cmd = ChildProcess.make('node', ['script.js'], {
|
|
106
113
|
// Working directory
|
|
@@ -141,8 +148,9 @@ The `fd3`/`fd4` names configure child-process stdio channels. Process handles ex
|
|
|
141
148
|
|
|
142
149
|
### Combinators
|
|
143
150
|
|
|
151
|
+
<!-- typecheck -->
|
|
144
152
|
```typescript
|
|
145
|
-
import { ChildProcess } from 'effect/
|
|
153
|
+
import { ChildProcess } from 'effect/process';
|
|
146
154
|
|
|
147
155
|
// Set cwd (applies to all commands in a pipeline)
|
|
148
156
|
const cmd = ChildProcess.make`ls -la`.pipe(ChildProcess.setCwd('/tmp'));
|
|
@@ -165,7 +173,7 @@ Commands are `Effect` values: `yield*` on a command evaluates through its `Effec
|
|
|
165
173
|
|
|
166
174
|
```typescript
|
|
167
175
|
import { Effect } from 'effect';
|
|
168
|
-
import { ChildProcessSpawner } from 'effect/
|
|
176
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
169
177
|
|
|
170
178
|
const program = Effect.gen(function* () {
|
|
171
179
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -175,9 +183,10 @@ const program = Effect.gen(function* () {
|
|
|
175
183
|
|
|
176
184
|
### Capture as String
|
|
177
185
|
|
|
186
|
+
<!-- typecheck -->
|
|
178
187
|
```typescript
|
|
179
188
|
import { Effect, String } from 'effect';
|
|
180
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
189
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
181
190
|
|
|
182
191
|
const program = Effect.gen(function* () {
|
|
183
192
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -190,39 +199,45 @@ const program = Effect.gen(function* () {
|
|
|
190
199
|
|
|
191
200
|
### Capture as Lines
|
|
192
201
|
|
|
202
|
+
<!-- typecheck -->
|
|
193
203
|
```typescript
|
|
194
204
|
import { Effect } from 'effect';
|
|
195
|
-
import
|
|
205
|
+
import * as Arr from 'effect/Array';
|
|
206
|
+
import * as Str from 'effect/String';
|
|
207
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
196
208
|
|
|
197
209
|
const program = Effect.gen(function* () {
|
|
198
210
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
199
211
|
const files = yield* spawner.lines(
|
|
200
212
|
ChildProcess.make('git', ['diff', '--name-only', 'main...HEAD'])
|
|
201
213
|
);
|
|
202
|
-
const tsFiles =
|
|
214
|
+
const tsFiles = Arr.filter(files, Str.endsWith('.ts'));
|
|
203
215
|
});
|
|
204
216
|
```
|
|
205
217
|
|
|
206
218
|
### Progressive Output with Side Effects
|
|
207
219
|
|
|
220
|
+
<!-- typecheck -->
|
|
208
221
|
```typescript
|
|
209
222
|
import { Effect, Stream } from 'effect';
|
|
210
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
223
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
211
224
|
|
|
212
225
|
const program = Effect.gen(function* () {
|
|
213
226
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
214
|
-
let output = '';
|
|
215
227
|
const handle = yield* spawner.spawn(
|
|
216
228
|
ChildProcess.make('bun', ['test'], {
|
|
217
229
|
extendEnv: true,
|
|
218
230
|
stdin: 'ignore'
|
|
219
231
|
})
|
|
220
232
|
);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
// Drain to EOF before leaving the scope, propagating read/progress failures.
|
|
234
|
+
const output = yield* handle.all.pipe(
|
|
235
|
+
Stream.decodeText(),
|
|
236
|
+
Stream.tap((chunk) => Effect.logDebug('Process output').pipe(
|
|
237
|
+
Effect.annotateLogs({ characters: chunk.length })
|
|
238
|
+
)),
|
|
239
|
+
Stream.mkString
|
|
240
|
+
);
|
|
226
241
|
const exitCode = yield* handle.exitCode;
|
|
227
242
|
return { output, exitCode };
|
|
228
243
|
}).pipe(Effect.scoped);
|
|
@@ -230,29 +245,41 @@ const program = Effect.gen(function* () {
|
|
|
230
245
|
|
|
231
246
|
Use this pattern when you need per-chunk side effects (progress reporting, streaming to UI) during command execution. `spawner.string`/`spawner.lines` cannot provide per-chunk callbacks.
|
|
232
247
|
|
|
248
|
+
Waiting for `handle.exitCode` alone does not drain output. If you fork a reader, join it before closing the scope and returning captured output. `Stream.mkString` buffers all text; for unbounded output, use `Stream.runForEach` without retaining it. Treat process output as potentially sensitive when choosing a progress callback.
|
|
249
|
+
|
|
233
250
|
### Get Exit Code
|
|
234
251
|
|
|
252
|
+
<!-- typecheck -->
|
|
235
253
|
```typescript
|
|
236
254
|
import { Effect } from 'effect';
|
|
237
|
-
import
|
|
255
|
+
import * as Schema from 'effect/Schema';
|
|
256
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
257
|
+
|
|
258
|
+
class CommandFailed extends Schema.TaggedError<CommandFailed>()('CommandFailed', {
|
|
259
|
+
exitCode: Schema.Number
|
|
260
|
+
}) {}
|
|
238
261
|
|
|
239
262
|
const program = Effect.gen(function* () {
|
|
240
263
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
241
264
|
const exitCode = yield* spawner.exitCode(
|
|
242
|
-
ChildProcess.make('test', ['-f', 'package.json']
|
|
265
|
+
ChildProcess.make('test', ['-f', 'package.json'], {
|
|
266
|
+
stdin: 'ignore', stdout: 'ignore', stderr: 'ignore'
|
|
267
|
+
})
|
|
243
268
|
);
|
|
244
269
|
// exitCode: ExitCode (branded number)
|
|
245
270
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
246
|
-
yield* Effect.fail(new
|
|
271
|
+
return yield* Effect.fail(new CommandFailed({ exitCode }));
|
|
247
272
|
}
|
|
248
273
|
});
|
|
249
274
|
```
|
|
250
275
|
|
|
276
|
+
`string`, `lines`, and stream helpers collect output; they do **not** reject a nonzero exit code. Use a scoped handle when success requires both captured output and a checked status. When using `exitCode` alone, inherit or ignore stdout/stderr so an unread pipe cannot fill and stall a noisy process. Output helpers default to stdout; pass `{ includeStderr: true }` to consume combined output, or configure stderr as `inherit`/`ignore` when only stdout is needed.
|
|
277
|
+
|
|
251
278
|
### Stream Output (Long-Running Processes)
|
|
252
279
|
|
|
253
280
|
```typescript
|
|
254
281
|
import { Console, Effect, Stream } from 'effect';
|
|
255
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
282
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
256
283
|
|
|
257
284
|
const program = Effect.gen(function* () {
|
|
258
285
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -273,9 +300,15 @@ const program = Effect.gen(function* () {
|
|
|
273
300
|
|
|
274
301
|
Use `spawner.spawn` when you need the process handle for interactive control, streaming output while running, or checking exit codes.
|
|
275
302
|
|
|
303
|
+
<!-- typecheck -->
|
|
276
304
|
```typescript
|
|
277
305
|
import { Console, Effect, Stream } from 'effect';
|
|
278
|
-
import
|
|
306
|
+
import * as Schema from 'effect/Schema';
|
|
307
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
308
|
+
|
|
309
|
+
class LintFailed extends Schema.TaggedError<LintFailed>()('LintFailed', {
|
|
310
|
+
exitCode: Schema.Number
|
|
311
|
+
}) {}
|
|
279
312
|
|
|
280
313
|
const program = Effect.gen(function* () {
|
|
281
314
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -298,7 +331,7 @@ const program = Effect.gen(function* () {
|
|
|
298
331
|
// Wait for exit
|
|
299
332
|
const exitCode = yield* handle.exitCode;
|
|
300
333
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
301
|
-
yield* Effect.fail(new
|
|
334
|
+
return yield* Effect.fail(new LintFailed({ exitCode }));
|
|
302
335
|
}
|
|
303
336
|
}).pipe(Effect.scoped); // <-- spawn requires Scope
|
|
304
337
|
```
|
|
@@ -310,7 +343,7 @@ const program = Effect.gen(function* () {
|
|
|
310
343
|
| `pid` | `ProcessId` | Process identifier (branded number) |
|
|
311
344
|
| `exitCode` | `Effect<ExitCode, PlatformError>` | Waits for exit, returns exit code |
|
|
312
345
|
| `isRunning` | `Effect<boolean, PlatformError>` | Check if still running |
|
|
313
|
-
| `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal;
|
|
346
|
+
| `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal; `forceKillAfter` bounds graceful waiting before escalation |
|
|
314
347
|
| `stdin` | `Sink<void, Uint8Array, never, PlatformError>` | Write to process stdin |
|
|
315
348
|
| `stdout` | `Stream<Uint8Array, PlatformError>` | Read process stdout |
|
|
316
349
|
| `stderr` | `Stream<Uint8Array, PlatformError>` | Read process stderr |
|
|
@@ -320,9 +353,10 @@ const program = Effect.gen(function* () {
|
|
|
320
353
|
|
|
321
354
|
## Piping Commands
|
|
322
355
|
|
|
356
|
+
<!-- typecheck -->
|
|
323
357
|
```typescript
|
|
324
358
|
import { Effect } from 'effect';
|
|
325
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
359
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
326
360
|
|
|
327
361
|
const program = Effect.gen(function* () {
|
|
328
362
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -371,12 +405,18 @@ const program = Effect.gen(function* () {
|
|
|
371
405
|
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer));
|
|
372
406
|
```
|
|
373
407
|
|
|
374
|
-
Or
|
|
408
|
+
Or compose the spawner with its FileSystem and Path dependencies:
|
|
375
409
|
|
|
376
410
|
```typescript
|
|
377
|
-
import { NodeChildProcessSpawner } from '@effect/platform-node';
|
|
411
|
+
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from '@effect/platform-node';
|
|
412
|
+
import { Effect, Layer } from 'effect';
|
|
413
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
378
414
|
|
|
379
|
-
const
|
|
415
|
+
declare const myEffect: Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner>;
|
|
416
|
+
const SpawnerLayer = NodeChildProcessSpawner.layer.pipe(
|
|
417
|
+
Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
|
|
418
|
+
);
|
|
419
|
+
const program = myEffect.pipe(Effect.provide(SpawnerLayer));
|
|
380
420
|
```
|
|
381
421
|
|
|
382
422
|
## Complete Example: DevTools Service
|
|
@@ -392,7 +432,8 @@ import {
|
|
|
392
432
|
Stream,
|
|
393
433
|
String
|
|
394
434
|
} from 'effect';
|
|
395
|
-
import
|
|
435
|
+
import * as Arr from 'effect/Array';
|
|
436
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
396
437
|
|
|
397
438
|
class DevToolsError extends Schema.TaggedError<DevToolsError>()(
|
|
398
439
|
'DevToolsError',
|
|
@@ -442,7 +483,7 @@ class DevTools extends Context.Service<
|
|
|
442
483
|
.pipe(
|
|
443
484
|
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
444
485
|
);
|
|
445
|
-
return
|
|
486
|
+
return Arr.filter(files, String.endsWith('.ts'));
|
|
446
487
|
});
|
|
447
488
|
|
|
448
489
|
const recentCommitSubjects = spawner
|
|
@@ -463,7 +504,7 @@ class DevTools extends Context.Service<
|
|
|
463
504
|
const runLintFix = Effect.gen(function* () {
|
|
464
505
|
const handle = yield* spawner
|
|
465
506
|
.spawn(
|
|
466
|
-
ChildProcess.make('bun', ['lint'], {
|
|
507
|
+
ChildProcess.make('bun', ['lint', '--fix'], {
|
|
467
508
|
env: { FORCE_COLOR: '1' },
|
|
468
509
|
extendEnv: true,
|
|
469
510
|
stdin: 'ignore'
|
|
@@ -484,11 +525,9 @@ class DevTools extends Context.Service<
|
|
|
484
525
|
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
485
526
|
);
|
|
486
527
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
487
|
-
return yield* new DevToolsError({
|
|
488
|
-
cause:
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
});
|
|
528
|
+
return yield* Effect.fail(new DevToolsError({
|
|
529
|
+
cause: { command: 'bun lint --fix', exitCode }
|
|
530
|
+
}));
|
|
492
531
|
}
|
|
493
532
|
}).pipe(Effect.scoped);
|
|
494
533
|
|
|
@@ -499,7 +538,9 @@ class DevTools extends Context.Service<
|
|
|
499
538
|
runLintFix
|
|
500
539
|
});
|
|
501
540
|
})
|
|
502
|
-
)
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
static readonly defaultLayer = DevTools.layer.pipe(Layer.provide(NodeServices.layer));
|
|
503
544
|
}
|
|
504
545
|
```
|
|
505
546
|
|
|
@@ -516,14 +557,14 @@ const program = Effect.gen(function* () {
|
|
|
516
557
|
}).pipe(Effect.scoped);
|
|
517
558
|
```
|
|
518
559
|
|
|
519
|
-
### DON'T:
|
|
560
|
+
### DON'T: Run a scoped program without providing its Scope
|
|
520
561
|
|
|
521
562
|
```typescript
|
|
522
|
-
//
|
|
563
|
+
// This definition is valid and retains Scope in its requirements.
|
|
523
564
|
const program = Effect.gen(function* () {
|
|
524
565
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
525
566
|
const handle = yield* spawner.spawn(ChildProcess.make`my-server`);
|
|
526
|
-
//
|
|
567
|
+
// The caller must own/provide Scope before running this program.
|
|
527
568
|
});
|
|
528
569
|
```
|
|
529
570
|
|
|
@@ -571,22 +612,15 @@ class MyService extends Context.Service<
|
|
|
571
612
|
>()('MyService') {}
|
|
572
613
|
```
|
|
573
614
|
|
|
574
|
-
### DO: Check
|
|
615
|
+
### DO: Check the exit status explicitly
|
|
575
616
|
|
|
576
617
|
```typescript
|
|
577
618
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
578
|
-
yield* Effect.fail(new
|
|
619
|
+
return yield* Effect.fail(new CommandFailed({ exitCode }));
|
|
579
620
|
}
|
|
580
621
|
```
|
|
581
622
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
// ❌ ExitCode is a branded type — direct number comparison may not work as expected
|
|
586
|
-
if (exitCode !== 0) {
|
|
587
|
-
/* ... */
|
|
588
|
-
}
|
|
589
|
-
```
|
|
623
|
+
`ExitCode` is a branded **number**, not a boxed runtime value. Both `exitCode !== 0` and comparison with `ChildProcessSpawner.ExitCode(0)` work. The constructor is useful when supplying an ExitCode to a typed API; the brand does not change numeric comparison semantics.
|
|
590
624
|
|
|
591
625
|
### DO: Use `handle.all` for interleaved stdout+stderr
|
|
592
626
|
|
|
@@ -610,9 +644,10 @@ yield* Stream.merge(handle.stdout, handle.all).pipe(Stream.runCollect);
|
|
|
610
644
|
|
|
611
645
|
Child process operations fail with `PlatformError`:
|
|
612
646
|
|
|
647
|
+
<!-- typecheck -->
|
|
613
648
|
```typescript
|
|
614
649
|
import { Effect } from 'effect';
|
|
615
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
650
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
616
651
|
|
|
617
652
|
const program = Effect.gen(function* () {
|
|
618
653
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -621,7 +656,9 @@ const program = Effect.gen(function* () {
|
|
|
621
656
|
.string(ChildProcess.make`non-existent-command`)
|
|
622
657
|
.pipe(
|
|
623
658
|
Effect.catchTag('PlatformError', (error) =>
|
|
624
|
-
|
|
659
|
+
error.reason._tag === 'NotFound'
|
|
660
|
+
? Effect.succeed('Optional command is unavailable')
|
|
661
|
+
: Effect.fail(error)
|
|
625
662
|
)
|
|
626
663
|
);
|
|
627
664
|
});
|
|
@@ -637,6 +674,7 @@ Keep process kill, timeout, and output-drain logic inside the process adapter th
|
|
|
637
674
|
|
|
638
675
|
Use `Effect.callback` to convert an `AbortSignal` (or any event-driven API) into an Effect. The cleanup Effect removes the listener, and the already-aborted edge case is handled first:
|
|
639
676
|
|
|
677
|
+
<!-- typecheck -->
|
|
640
678
|
```typescript
|
|
641
679
|
import { Effect } from 'effect';
|
|
642
680
|
|
|
@@ -651,16 +689,25 @@ const fromAbortSignal = (signal: AbortSignal) =>
|
|
|
651
689
|
|
|
652
690
|
### Abort / Timeout Multiplexing
|
|
653
691
|
|
|
654
|
-
Combine `Effect.raceAll` with discriminated result types to handle exit, abort, and timeout in a single expression:
|
|
692
|
+
Combine `Effect.raceAll` with discriminated result types to handle successful exit, abort, and timeout in a single expression. `raceAll` waits for the first success, so turn exit observation into an `Exit` value to preserve a failed exit-code read rather than hiding it behind the timeout:
|
|
655
693
|
|
|
694
|
+
<!-- typecheck -->
|
|
656
695
|
```typescript
|
|
657
|
-
import { Effect } from 'effect';
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
696
|
+
import { Duration, Effect } from 'effect';
|
|
697
|
+
import { ChildProcessSpawner } from 'effect/process';
|
|
698
|
+
|
|
699
|
+
declare const fromAbortSignal: (signal: AbortSignal) => Effect.Effect<void>;
|
|
700
|
+
|
|
701
|
+
// Call within the scope that owns the handle and its output reader.
|
|
702
|
+
const awaitOutcome = Effect.fn('Process.awaitOutcome')(function* (
|
|
703
|
+
handle: ChildProcessSpawner.ChildProcessHandle,
|
|
704
|
+
signal: AbortSignal,
|
|
705
|
+
timeout: Duration.Input
|
|
706
|
+
) {
|
|
707
|
+
const exit = yield* Effect.raceAll([
|
|
662
708
|
handle.exitCode.pipe(
|
|
663
|
-
Effect.
|
|
709
|
+
Effect.exit,
|
|
710
|
+
Effect.map((result) => ({ kind: 'exit' as const, result }))
|
|
664
711
|
),
|
|
665
712
|
fromAbortSignal(signal).pipe(
|
|
666
713
|
Effect.map(() => ({ kind: 'abort' as const }))
|
|
@@ -669,11 +716,15 @@ const exit =
|
|
|
669
716
|
Effect.map(() => ({ kind: 'timeout' as const }))
|
|
670
717
|
)
|
|
671
718
|
]);
|
|
672
|
-
if (exit.kind !== 'exit') {
|
|
673
|
-
|
|
674
|
-
}
|
|
719
|
+
if (exit.kind !== 'exit') {
|
|
720
|
+
yield* handle.kill({ forceKillAfter: '3 seconds' });
|
|
721
|
+
}
|
|
722
|
+
return exit;
|
|
723
|
+
});
|
|
675
724
|
```
|
|
676
725
|
|
|
726
|
+
Handle `exit.result` with `Exit.match` when `kind === 'exit'`; it includes observation failures. This race only chooses the control outcome: drain/join any output reader separately within the owning scope before returning captured output.
|
|
727
|
+
|
|
677
728
|
## Related Skills
|
|
678
729
|
|
|
679
730
|
- **effect-platform-abstraction**: FileSystem, Path, and other platform services
|