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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. 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