opencode-effect-enforcer 0.2.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/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-command-executor
|
|
3
|
+
description: Spawn and manage child processes using Effect's ChildProcess module. Use this skill when running shell commands, capturing process output, piping commands, streaming long-running process output, or managing process lifecycles with scoped cleanup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Child Process Execution with Effect v4
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
**When to use this skill:**
|
|
13
|
+
|
|
14
|
+
- Running shell commands or external programs
|
|
15
|
+
- Capturing command output as string, lines, or stream
|
|
16
|
+
- Piping commands together (shell pipeline equivalent)
|
|
17
|
+
- Streaming output from long-running processes
|
|
18
|
+
- Managing process lifecycles with scoped cleanup
|
|
19
|
+
|
|
20
|
+
**Note:** This skill covers child process execution, NOT `@effect/cli` for building CLI applications.
|
|
21
|
+
|
|
22
|
+
## Import Pattern
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Platform layer (Node.js):
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { NodeServices } from '@effect/platform-node';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Creating Commands
|
|
35
|
+
|
|
36
|
+
### Template Literal Form
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { ChildProcess } from 'effect/unstable/process';
|
|
40
|
+
|
|
41
|
+
// Simple command — parsed by whitespace
|
|
42
|
+
const cmd = ChildProcess.make`echo hello world`;
|
|
43
|
+
|
|
44
|
+
// With interpolation — expressions become separate arguments
|
|
45
|
+
const name = 'my-package';
|
|
46
|
+
const cmd2 = ChildProcess.make`bun publish ${name}`;
|
|
47
|
+
|
|
48
|
+
// Array expressions expand to multiple arguments
|
|
49
|
+
const files = ['a.ts', 'b.ts', 'c.ts'];
|
|
50
|
+
const cmd3 = ChildProcess.make`oxfmt ${files}`;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Options + Template Literal Form
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { ChildProcess } from 'effect/unstable/process';
|
|
57
|
+
|
|
58
|
+
// Options object returns a tagged template function
|
|
59
|
+
const cmd = ChildProcess.make({ cwd: '/tmp' })`ls -la`;
|
|
60
|
+
|
|
61
|
+
const cmd2 = ChildProcess.make({
|
|
62
|
+
cwd: '/app',
|
|
63
|
+
env: { NODE_ENV: 'production' },
|
|
64
|
+
extendEnv: true
|
|
65
|
+
})`node server.js`;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Array Form
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { ChildProcess } from 'effect/unstable/process';
|
|
72
|
+
|
|
73
|
+
// Explicit command + args array
|
|
74
|
+
const cmd = ChildProcess.make('git', ['status'], {
|
|
75
|
+
extendEnv: true,
|
|
76
|
+
stdin: 'ignore'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// With options
|
|
80
|
+
const cmd2 = ChildProcess.make('bun', ['lint'], {
|
|
81
|
+
env: { FORCE_COLOR: '1' },
|
|
82
|
+
extendEnv: true,
|
|
83
|
+
stdin: 'ignore'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Command only (no args)
|
|
87
|
+
const cmd3 = ChildProcess.make('node', {
|
|
88
|
+
cwd: '/app',
|
|
89
|
+
extendEnv: true,
|
|
90
|
+
stdin: 'ignore'
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Command Options
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { ChildProcess } from 'effect/unstable/process';
|
|
98
|
+
|
|
99
|
+
const cmd = ChildProcess.make('node', ['script.js'], {
|
|
100
|
+
// Working directory
|
|
101
|
+
cwd: '/path/to/project',
|
|
102
|
+
|
|
103
|
+
// Environment variables
|
|
104
|
+
env: { NODE_ENV: 'production', API_KEY: 'xyz' },
|
|
105
|
+
|
|
106
|
+
// Merge with process.env (default: false)
|
|
107
|
+
extendEnv: true,
|
|
108
|
+
|
|
109
|
+
// Run inside a shell (generally disadvised)
|
|
110
|
+
shell: false,
|
|
111
|
+
|
|
112
|
+
// Detach from parent (default: true on non-Windows)
|
|
113
|
+
detached: true,
|
|
114
|
+
|
|
115
|
+
// stdio configuration — use 'ignore' for non-interactive commands
|
|
116
|
+
stdin: 'ignore', // "pipe" | "inherit" | "ignore" | Stream
|
|
117
|
+
stdout: 'pipe', // "pipe" | "inherit" | "ignore" | Sink
|
|
118
|
+
stderr: 'pipe', // "pipe" | "inherit" | "ignore" | Sink
|
|
119
|
+
|
|
120
|
+
// Kill signal defaults
|
|
121
|
+
killSignal: 'SIGTERM',
|
|
122
|
+
forceKillAfter: '3 seconds',
|
|
123
|
+
|
|
124
|
+
// Additional file descriptors
|
|
125
|
+
additionalFds: {
|
|
126
|
+
fd3: { type: 'output' }, // readable by parent
|
|
127
|
+
fd4: { type: 'input' } // writable by parent
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`extendEnv` defaults to `false`. If `env` is supplied without `extendEnv: true`, it replaces the inherited child environment rather than merging with it.
|
|
133
|
+
|
|
134
|
+
The `fd3`/`fd4` names above configure child-process stdio channels. They are unrelated to the `FileSystem.File.Descriptor` type removed in beta.103; process handles still expose `getInputFd(number)` and `getOutputFd(number)` for configured additional descriptors.
|
|
135
|
+
|
|
136
|
+
### Combinators
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { ChildProcess } from 'effect/unstable/process';
|
|
140
|
+
|
|
141
|
+
// Set cwd (applies to all commands in a pipeline)
|
|
142
|
+
const cmd = ChildProcess.make`ls -la`.pipe(ChildProcess.setCwd('/tmp'));
|
|
143
|
+
|
|
144
|
+
// Set env (applies to all commands in a pipeline)
|
|
145
|
+
const cmd2 = ChildProcess.make`node script.js`.pipe(
|
|
146
|
+
ChildProcess.setEnv({ NODE_ENV: 'test' })
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Prefix a command (e.g. `time`, `nice`, `sudo`)
|
|
150
|
+
const cmd3 = ChildProcess.make`echo foo`.pipe(ChildProcess.prefix`time`);
|
|
151
|
+
// executes: time echo foo
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Executing Commands
|
|
155
|
+
|
|
156
|
+
Commands are `Effect` values: `yield*` on a command evaluates through its `Effectable` implementation, calls `ChildProcessSpawner.spawn`, and returns a `ChildProcessHandle`. `spawner.spawn` still requires `Scope`; helpers such as `string`, `lines`, and `exitCode` manage scope internally.
|
|
157
|
+
|
|
158
|
+
### Get the Spawner Service
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { Effect } from 'effect';
|
|
162
|
+
import { ChildProcessSpawner } from 'effect/unstable/process';
|
|
163
|
+
|
|
164
|
+
const program = Effect.gen(function* () {
|
|
165
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
166
|
+
// use spawner.string, spawner.lines, spawner.spawn, etc.
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Capture as String
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { Effect, String } from 'effect';
|
|
174
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
175
|
+
|
|
176
|
+
const program = Effect.gen(function* () {
|
|
177
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
178
|
+
const version = yield* spawner
|
|
179
|
+
.string(ChildProcess.make('node', ['--version']))
|
|
180
|
+
.pipe(Effect.map(String.trim));
|
|
181
|
+
// version: "v22.0.0"
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Capture as Lines
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { Effect } from 'effect';
|
|
189
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
190
|
+
|
|
191
|
+
const program = Effect.gen(function* () {
|
|
192
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
193
|
+
const files = yield* spawner.lines(
|
|
194
|
+
ChildProcess.make('git', ['diff', '--name-only', 'main...HEAD'])
|
|
195
|
+
);
|
|
196
|
+
const tsFiles = files.filter((f) => f.endsWith('.ts'));
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Progressive Output with Side Effects
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { Effect, Stream } from 'effect';
|
|
204
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
205
|
+
|
|
206
|
+
const program = Effect.gen(function* () {
|
|
207
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
208
|
+
let output = '';
|
|
209
|
+
const handle = yield* spawner.spawn(
|
|
210
|
+
ChildProcess.make('bun', ['test'], {
|
|
211
|
+
extendEnv: true,
|
|
212
|
+
stdin: 'ignore'
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
|
|
216
|
+
Effect.sync(() => {
|
|
217
|
+
output += chunk;
|
|
218
|
+
})
|
|
219
|
+
).pipe(Effect.forkScoped);
|
|
220
|
+
const exitCode = yield* handle.exitCode;
|
|
221
|
+
return { output, exitCode };
|
|
222
|
+
}).pipe(Effect.scoped);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
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.
|
|
226
|
+
|
|
227
|
+
### Get Exit Code
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { Effect } from 'effect';
|
|
231
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
232
|
+
|
|
233
|
+
const program = Effect.gen(function* () {
|
|
234
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
235
|
+
const exitCode = yield* spawner.exitCode(
|
|
236
|
+
ChildProcess.make('test', ['-f', 'package.json'])
|
|
237
|
+
);
|
|
238
|
+
// exitCode: ExitCode (branded number)
|
|
239
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
240
|
+
yield* Effect.fail(new Error('file not found'));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Stream Output (Long-Running Processes)
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { Console, Effect, Stream } from 'effect';
|
|
249
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
250
|
+
|
|
251
|
+
const program = Effect.gen(function* () {
|
|
252
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
253
|
+
|
|
254
|
+
// Stream lines from a command
|
|
255
|
+
yield* spawner
|
|
256
|
+
.streamLines(ChildProcess.make`tail -f /var/log/app.log`)
|
|
257
|
+
.pipe(Stream.runForEach((line) => Console.log(line)));
|
|
258
|
+
|
|
259
|
+
// Stream raw string chunks
|
|
260
|
+
yield* spawner
|
|
261
|
+
.streamString(ChildProcess.make`my-program`)
|
|
262
|
+
.pipe(Stream.runForEach((chunk) => Console.log(chunk)));
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Process Handle (spawn)
|
|
267
|
+
|
|
268
|
+
Use `spawner.spawn` when you need the process handle for interactive control, streaming output while running, or checking exit codes.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { Console, Effect, Stream } from 'effect';
|
|
272
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
273
|
+
|
|
274
|
+
const program = Effect.gen(function* () {
|
|
275
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
276
|
+
|
|
277
|
+
const handle = yield* spawner.spawn(
|
|
278
|
+
ChildProcess.make('bun', ['lint'], {
|
|
279
|
+
env: { FORCE_COLOR: '1' },
|
|
280
|
+
extendEnv: true,
|
|
281
|
+
stdin: 'ignore'
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Stream combined stdout+stderr while process runs
|
|
286
|
+
yield* handle.all.pipe(
|
|
287
|
+
Stream.decodeText(),
|
|
288
|
+
Stream.splitLines,
|
|
289
|
+
Stream.runForEach((line) => Console.log(`[lint] ${line}`))
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Wait for exit
|
|
293
|
+
const exitCode = yield* handle.exitCode;
|
|
294
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
295
|
+
yield* Effect.fail(new Error(`lint failed: exit ${exitCode}`));
|
|
296
|
+
}
|
|
297
|
+
}).pipe(Effect.scoped); // <-- spawn requires Scope
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### ChildProcessHandle API
|
|
301
|
+
|
|
302
|
+
| Property/Method | Type | Description |
|
|
303
|
+
| ----------------- | ---------------------------------------------- | -------------------------------------------------------------------------- |
|
|
304
|
+
| `pid` | `ProcessId` | Process identifier (branded number) |
|
|
305
|
+
| `exitCode` | `Effect<ExitCode, PlatformError>` | Waits for exit, returns exit code |
|
|
306
|
+
| `isRunning` | `Effect<boolean, PlatformError>` | Check if still running |
|
|
307
|
+
| `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal; pass `{ forceKillAfter: '3 seconds' }` to ensure cleanup |
|
|
308
|
+
| `stdin` | `Sink<void, Uint8Array, never, PlatformError>` | Write to process stdin |
|
|
309
|
+
| `stdout` | `Stream<Uint8Array, PlatformError>` | Read process stdout |
|
|
310
|
+
| `stderr` | `Stream<Uint8Array, PlatformError>` | Read process stderr |
|
|
311
|
+
| `all` | `Stream<Uint8Array, PlatformError>` | Interleaved stdout+stderr |
|
|
312
|
+
| `getInputFd(fd)` | `Sink<void, Uint8Array, ...>` | Write to additional fd |
|
|
313
|
+
| `getOutputFd(fd)` | `Stream<Uint8Array, ...>` | Read from additional fd |
|
|
314
|
+
|
|
315
|
+
## Piping Commands
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { Effect } from 'effect';
|
|
319
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
320
|
+
|
|
321
|
+
const program = Effect.gen(function* () {
|
|
322
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
323
|
+
|
|
324
|
+
// stdout → stdin (default)
|
|
325
|
+
const lines = yield* spawner.lines(
|
|
326
|
+
ChildProcess.make('git', [
|
|
327
|
+
'log',
|
|
328
|
+
'--pretty=format:%s',
|
|
329
|
+
'-n',
|
|
330
|
+
'20'
|
|
331
|
+
]).pipe(ChildProcess.pipeTo(ChildProcess.make('head', ['-n', '5'])))
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Pipe stderr instead of stdout
|
|
335
|
+
const errors = yield* spawner.lines(
|
|
336
|
+
ChildProcess.make`my-program`.pipe(
|
|
337
|
+
ChildProcess.pipeTo(ChildProcess.make`grep error`, {
|
|
338
|
+
from: 'stderr'
|
|
339
|
+
})
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Pipe combined stdout+stderr
|
|
344
|
+
const all = yield* spawner.lines(
|
|
345
|
+
ChildProcess.make`my-program`.pipe(
|
|
346
|
+
ChildProcess.pipeTo(ChildProcess.make`tee output.log`, {
|
|
347
|
+
from: 'all'
|
|
348
|
+
})
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Providing the Platform Layer
|
|
355
|
+
|
|
356
|
+
`ChildProcess` commands require a `ChildProcessSpawner` implementation. In Node.js:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { NodeServices } from '@effect/platform-node';
|
|
360
|
+
import { Effect } from 'effect';
|
|
361
|
+
|
|
362
|
+
// NodeServices.layer provides: ChildProcessSpawner, Crypto, FileSystem, Path, Stdio, Terminal
|
|
363
|
+
const program = Effect.gen(function* () {
|
|
364
|
+
// ...
|
|
365
|
+
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer));
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Or provide just the spawner:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { NodeChildProcessSpawner } from '@effect/platform-node';
|
|
372
|
+
|
|
373
|
+
const program = myEffect.pipe(Effect.provide(NodeChildProcessSpawner.layer));
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Complete Example: DevTools Service
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { NodeServices } from '@effect/platform-node';
|
|
380
|
+
import {
|
|
381
|
+
Console,
|
|
382
|
+
Effect,
|
|
383
|
+
Layer,
|
|
384
|
+
Schema,
|
|
385
|
+
Context,
|
|
386
|
+
Stream,
|
|
387
|
+
String
|
|
388
|
+
} from 'effect';
|
|
389
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
390
|
+
|
|
391
|
+
class DevToolsError extends Schema.TaggedError<DevToolsError>()(
|
|
392
|
+
'DevToolsError',
|
|
393
|
+
{
|
|
394
|
+
cause: Schema.Defect()
|
|
395
|
+
}
|
|
396
|
+
) {}
|
|
397
|
+
|
|
398
|
+
class DevTools extends Context.Service<
|
|
399
|
+
DevTools,
|
|
400
|
+
{
|
|
401
|
+
readonly nodeVersion: Effect.Effect<string, DevToolsError>;
|
|
402
|
+
readonly recentCommitSubjects: Effect.Effect<
|
|
403
|
+
ReadonlyArray<string>,
|
|
404
|
+
DevToolsError
|
|
405
|
+
>;
|
|
406
|
+
readonly runLintFix: Effect.Effect<void, DevToolsError>;
|
|
407
|
+
changedTypeScriptFiles(
|
|
408
|
+
baseRef: string
|
|
409
|
+
): Effect.Effect<ReadonlyArray<string>, DevToolsError>;
|
|
410
|
+
}
|
|
411
|
+
>()('app/DevTools') {
|
|
412
|
+
static readonly layer = Layer.effect(
|
|
413
|
+
DevTools,
|
|
414
|
+
Effect.gen(function* () {
|
|
415
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
416
|
+
|
|
417
|
+
const nodeVersion = spawner
|
|
418
|
+
.string(ChildProcess.make('node', ['--version']))
|
|
419
|
+
.pipe(
|
|
420
|
+
Effect.map(String.trim),
|
|
421
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const changedTypeScriptFiles = Effect.fn(
|
|
425
|
+
'DevTools.changedTypeScriptFiles'
|
|
426
|
+
)(function* (baseRef: string) {
|
|
427
|
+
yield* Effect.annotateCurrentSpan({ baseRef });
|
|
428
|
+
const files = yield* spawner
|
|
429
|
+
.lines(
|
|
430
|
+
ChildProcess.make('git', [
|
|
431
|
+
'diff',
|
|
432
|
+
'--name-only',
|
|
433
|
+
`${baseRef}...HEAD`
|
|
434
|
+
])
|
|
435
|
+
)
|
|
436
|
+
.pipe(
|
|
437
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
438
|
+
);
|
|
439
|
+
return files.filter((file) => file.endsWith('.ts'));
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const recentCommitSubjects = spawner
|
|
443
|
+
.lines(
|
|
444
|
+
ChildProcess.make('git', [
|
|
445
|
+
'log',
|
|
446
|
+
'--pretty=format:%s',
|
|
447
|
+
'-n',
|
|
448
|
+
'20'
|
|
449
|
+
]).pipe(
|
|
450
|
+
ChildProcess.pipeTo(
|
|
451
|
+
ChildProcess.make('head', ['-n', '5'])
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
.pipe(Effect.mapError((cause) => new DevToolsError({ cause })));
|
|
456
|
+
|
|
457
|
+
const runLintFix = Effect.gen(function* () {
|
|
458
|
+
const handle = yield* spawner
|
|
459
|
+
.spawn(
|
|
460
|
+
ChildProcess.make('bun', ['lint'], {
|
|
461
|
+
env: { FORCE_COLOR: '1' },
|
|
462
|
+
extendEnv: true,
|
|
463
|
+
stdin: 'ignore'
|
|
464
|
+
})
|
|
465
|
+
)
|
|
466
|
+
.pipe(
|
|
467
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
yield* handle.all.pipe(
|
|
471
|
+
Stream.decodeText(),
|
|
472
|
+
Stream.splitLines,
|
|
473
|
+
Stream.runForEach((line) => Console.log(`[lint] ${line}`)),
|
|
474
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const exitCode = yield* handle.exitCode.pipe(
|
|
478
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
479
|
+
);
|
|
480
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
481
|
+
return yield* new DevToolsError({
|
|
482
|
+
cause: new Error(
|
|
483
|
+
`bun lint failed with exit code ${exitCode}`
|
|
484
|
+
)
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}).pipe(Effect.scoped);
|
|
488
|
+
|
|
489
|
+
return DevTools.of({
|
|
490
|
+
nodeVersion,
|
|
491
|
+
changedTypeScriptFiles,
|
|
492
|
+
recentCommitSubjects,
|
|
493
|
+
runLintFix
|
|
494
|
+
});
|
|
495
|
+
})
|
|
496
|
+
).pipe(Layer.provide(NodeServices.layer));
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## DO / DON'T
|
|
501
|
+
|
|
502
|
+
### DO: Use `Effect.scoped` when calling `spawner.spawn`
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// spawn returns a handle that requires Scope for lifecycle management
|
|
506
|
+
const program = Effect.gen(function* () {
|
|
507
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
508
|
+
const handle = yield* spawner.spawn(ChildProcess.make`my-server`);
|
|
509
|
+
// handle is automatically cleaned up when scope closes
|
|
510
|
+
}).pipe(Effect.scoped);
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### DON'T: Use `spawner.spawn` without scoping
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// Process handle leaks — no scope to manage cleanup
|
|
517
|
+
const program = Effect.gen(function* () {
|
|
518
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
519
|
+
const handle = yield* spawner.spawn(ChildProcess.make`my-server`);
|
|
520
|
+
// ❌ no Effect.scoped — process may leak
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### DO: Use `spawner.string` / `spawner.lines` for simple output capture
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// These convenience methods handle scope internally
|
|
528
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
529
|
+
const output = yield* spawner.string(ChildProcess.make`echo hello`);
|
|
530
|
+
const lines = yield* spawner.lines(ChildProcess.make`ls -1`);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### DON'T: Use `spawn` + manual stream collection when `string`/`lines` suffices
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// Unnecessarily complex for simple output capture
|
|
537
|
+
const handle = yield* spawner.spawn(ChildProcess.make`echo hello`);
|
|
538
|
+
const chunks = yield* Stream.runCollect(handle.stdout);
|
|
539
|
+
// ❌ overkill — just use spawner.string
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### DO: Use `Effect.mapError` to wrap `PlatformError` in domain errors
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
class MyError extends Schema.TaggedError<MyError>()('MyError', {
|
|
546
|
+
cause: Schema.Defect()
|
|
547
|
+
}) {}
|
|
548
|
+
|
|
549
|
+
const result =
|
|
550
|
+
yield*
|
|
551
|
+
spawner
|
|
552
|
+
.string(ChildProcess.make`git status`)
|
|
553
|
+
.pipe(Effect.mapError((cause) => new MyError({ cause })));
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### DON'T: Let `PlatformError` leak into your service API
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
// ❌ exposes platform implementation details to consumers
|
|
560
|
+
class MyService extends Context.Service<
|
|
561
|
+
MyService,
|
|
562
|
+
{
|
|
563
|
+
readonly status: Effect.Effect<string, PlatformError.PlatformError>; // ❌
|
|
564
|
+
}
|
|
565
|
+
>()('MyService') {}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### DO: Check `ExitCode` with the branded constructor
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
572
|
+
yield* Effect.fail(new Error(`command failed: ${exitCode}`));
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### DON'T: Compare exit codes as raw numbers
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
// ❌ ExitCode is a branded type — direct number comparison may not work as expected
|
|
580
|
+
if (exitCode !== 0) {
|
|
581
|
+
/* ... */
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### DO: Use `handle.all` for interleaved stdout+stderr
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
yield*
|
|
589
|
+
handle.all.pipe(
|
|
590
|
+
Stream.decodeText(),
|
|
591
|
+
Stream.splitLines,
|
|
592
|
+
Stream.runForEach((line) => Console.log(line))
|
|
593
|
+
);
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### DON'T: Mix `handle.stdout`/`handle.stderr` with `handle.all`
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// ❌ Using stdout/stderr alongside all may cause interleaving issues
|
|
600
|
+
yield* Stream.merge(handle.stdout, handle.all).pipe(Stream.runCollect);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Error Handling
|
|
604
|
+
|
|
605
|
+
Child process operations fail with `PlatformError`:
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
import { Effect } from 'effect';
|
|
609
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
610
|
+
|
|
611
|
+
const program = Effect.gen(function* () {
|
|
612
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
613
|
+
|
|
614
|
+
const result = yield* spawner
|
|
615
|
+
.string(ChildProcess.make`non-existent-command`)
|
|
616
|
+
.pipe(
|
|
617
|
+
Effect.catchTag('PlatformError', (error) =>
|
|
618
|
+
Effect.succeed(`fallback: ${error.message}`)
|
|
619
|
+
)
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
## Boundary Abort Signals
|
|
625
|
+
|
|
626
|
+
Prefer Effect interruption as the cancellation signal between your own services. If a host API hands you an `AbortSignal`, convert it once at the outer boundary instead of threading `AbortSignal` through unrelated services.
|
|
627
|
+
|
|
628
|
+
Keep process kill, timeout, and output-drain logic inside the process adapter that owns the `ChildProcessHandle`.
|
|
629
|
+
|
|
630
|
+
## Bridging External Signals
|
|
631
|
+
|
|
632
|
+
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:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
import { Effect } from 'effect';
|
|
636
|
+
|
|
637
|
+
const fromAbortSignal = (signal: AbortSignal) =>
|
|
638
|
+
Effect.callback<void>((resume) => {
|
|
639
|
+
if (signal.aborted) return resume(Effect.void);
|
|
640
|
+
const handler = () => resume(Effect.void);
|
|
641
|
+
signal.addEventListener('abort', handler, { once: true });
|
|
642
|
+
return Effect.sync(() => signal.removeEventListener('abort', handler));
|
|
643
|
+
});
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Abort / Timeout Multiplexing
|
|
647
|
+
|
|
648
|
+
Combine `Effect.raceAll` with discriminated result types to handle exit, abort, and timeout in a single expression:
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
import { Effect } from 'effect';
|
|
652
|
+
|
|
653
|
+
const exit =
|
|
654
|
+
yield*
|
|
655
|
+
Effect.raceAll([
|
|
656
|
+
handle.exitCode.pipe(
|
|
657
|
+
Effect.map((code) => ({ kind: 'exit' as const, code }))
|
|
658
|
+
),
|
|
659
|
+
fromAbortSignal(signal).pipe(
|
|
660
|
+
Effect.map(() => ({ kind: 'abort' as const }))
|
|
661
|
+
),
|
|
662
|
+
Effect.sleep(timeout).pipe(
|
|
663
|
+
Effect.map(() => ({ kind: 'timeout' as const }))
|
|
664
|
+
)
|
|
665
|
+
]);
|
|
666
|
+
if (exit.kind !== 'exit') {
|
|
667
|
+
yield* handle.kill({ forceKillAfter: '3 seconds' });
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Related Skills
|
|
672
|
+
|
|
673
|
+
- **effect-platform-abstraction**: FileSystem, Path, and other platform services
|
|
674
|
+
- **effect-testing**: Testing Effect programs with @effect/vitest
|
|
675
|
+
- **effect-error-handling**: Typed error handling patterns with catchTag
|