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,523 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-cli
|
|
3
|
+
description: Build type-safe CLI applications using Effect CLI module for argument parsing, options, commands, and dependency injection.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effect CLI (v4)
|
|
7
|
+
|
|
8
|
+
Build type-safe command-line applications with typed arguments, flags, subcommands, and dependency injection.
|
|
9
|
+
|
|
10
|
+
## Import Pattern
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { Argument, Command, Flag, Prompt } from 'effect/unstable/cli';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Platform services and runtime for the entry point:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { NodeRuntime, NodeServices } from '@effect/platform-node';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Positional Arguments (Argument)
|
|
23
|
+
|
|
24
|
+
Positional arguments are parsed in order. `Argument.boolean` intentionally does not exist — use `Flag.boolean` or `Argument.choice("name", ["true", "false"])` instead.
|
|
25
|
+
|
|
26
|
+
### Constructors
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Argument } from 'effect/unstable/cli';
|
|
30
|
+
|
|
31
|
+
Argument.string('name'); // string
|
|
32
|
+
Argument.integer('count'); // number (integer)
|
|
33
|
+
Argument.float('ratio'); // number (float)
|
|
34
|
+
Argument.date('deadline'); // Date
|
|
35
|
+
Argument.file('input'); // file path (string)
|
|
36
|
+
Argument.file('input', { mustExist: true }); // file path that must exist
|
|
37
|
+
Argument.directory('dir'); // directory path (string)
|
|
38
|
+
Argument.directory('dir', { mustExist: true }); // directory that must exist
|
|
39
|
+
Argument.path('target'); // any path (string)
|
|
40
|
+
Argument.choice('env', ['dev', 'staging', 'prod']); // constrained string union
|
|
41
|
+
Argument.choiceWithValue('level', [
|
|
42
|
+
// choice with mapped values
|
|
43
|
+
['debug', 0],
|
|
44
|
+
['info', 1],
|
|
45
|
+
['error', 3]
|
|
46
|
+
]);
|
|
47
|
+
Argument.redacted('secret'); // Redacted<string>
|
|
48
|
+
Argument.fileText('config'); // reads file content as string
|
|
49
|
+
Argument.fileParse('config'); // reads and parses file (auto-detects format)
|
|
50
|
+
Argument.fileSchema('config', MySchema); // reads and validates file via Schema
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Combinators
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Argument } from 'effect/unstable/cli';
|
|
57
|
+
|
|
58
|
+
// Description for help text
|
|
59
|
+
Argument.string('file').pipe(Argument.withDescription('Input file'));
|
|
60
|
+
|
|
61
|
+
// Default value
|
|
62
|
+
Argument.integer('port').pipe(Argument.withDefault(8080));
|
|
63
|
+
|
|
64
|
+
// Optional (returns Option<T>)
|
|
65
|
+
Argument.string('config').pipe(Argument.optional);
|
|
66
|
+
|
|
67
|
+
// Variadic (returns ReadonlyArray<T>)
|
|
68
|
+
Argument.string('files').pipe(Argument.variadic);
|
|
69
|
+
Argument.string('files').pipe(Argument.variadic({ min: 1 }));
|
|
70
|
+
Argument.string('files').pipe(Argument.variadic({ min: 1, max: 5 }));
|
|
71
|
+
|
|
72
|
+
// Direct variadic form is also supported
|
|
73
|
+
Argument.variadic(Argument.string('files'));
|
|
74
|
+
Argument.variadic(Argument.string('files'), { min: 1 });
|
|
75
|
+
|
|
76
|
+
// Cardinality shortcuts
|
|
77
|
+
Argument.string('files').pipe(Argument.atLeast(1));
|
|
78
|
+
Argument.string('files').pipe(Argument.atMost(5));
|
|
79
|
+
Argument.string('files').pipe(Argument.between(1, 5));
|
|
80
|
+
|
|
81
|
+
// Transform
|
|
82
|
+
Argument.integer('port').pipe(Argument.map((p) => `http://localhost:${p}`));
|
|
83
|
+
|
|
84
|
+
// Validate with Schema
|
|
85
|
+
Argument.string('input').pipe(Argument.withSchema(Schema.NonEmptyString));
|
|
86
|
+
|
|
87
|
+
// Fallback from env config
|
|
88
|
+
Argument.string('repo').pipe(
|
|
89
|
+
Argument.withFallbackConfig(Config.string('REPOSITORY'))
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Fallback interactive prompt
|
|
93
|
+
Argument.string('name').pipe(
|
|
94
|
+
Argument.withFallbackPrompt(Prompt.text({ message: 'Name' }))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Custom metavar for help text
|
|
98
|
+
Argument.integer('port').pipe(Argument.withMetavar('PORT'));
|
|
99
|
+
|
|
100
|
+
// Filter with error message
|
|
101
|
+
Argument.integer('count').pipe(
|
|
102
|
+
Argument.filter(
|
|
103
|
+
(n) => n > 0,
|
|
104
|
+
(n) => `Expected positive, got ${n}`
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Named Flags (Flag)
|
|
110
|
+
|
|
111
|
+
Flags are named options with `--name` or `-alias` syntax.
|
|
112
|
+
|
|
113
|
+
### Constructors
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { Flag } from 'effect/unstable/cli';
|
|
117
|
+
|
|
118
|
+
Flag.boolean('verbose'); // required: --verbose / --no-verbose; omission fails
|
|
119
|
+
Flag.string('config'); // --config value
|
|
120
|
+
Flag.integer('port'); // --port 8080
|
|
121
|
+
Flag.float('rate'); // --rate 3.14
|
|
122
|
+
Flag.date('since'); // --since 2024-01-01
|
|
123
|
+
Flag.file('input'); // --input file.txt
|
|
124
|
+
Flag.file('input', { mustExist: true }); // file must exist
|
|
125
|
+
Flag.directory('output'); // --output ./dist
|
|
126
|
+
Flag.path('config-path'); // --config-path /etc/app
|
|
127
|
+
Flag.choice('env', ['dev', 'staging', 'prod']); // --env dev
|
|
128
|
+
Flag.choiceWithValue('log-level', [
|
|
129
|
+
// choice with mapped values
|
|
130
|
+
['debug', 'Debug' as const],
|
|
131
|
+
['info', 'Info' as const],
|
|
132
|
+
['error', 'Error' as const]
|
|
133
|
+
]);
|
|
134
|
+
Flag.redacted('password'); // Redacted<string>
|
|
135
|
+
Flag.fileText('config-file'); // reads file content
|
|
136
|
+
Flag.fileParse('config'); // reads and parses file (auto-detects format)
|
|
137
|
+
Flag.fileSchema('config', MySchema); // reads and validates via Schema
|
|
138
|
+
Flag.keyValuePair('env'); // --env FOO=bar → Record<string, string>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Combinators
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { Flag } from 'effect/unstable/cli';
|
|
145
|
+
|
|
146
|
+
// Alias
|
|
147
|
+
Flag.boolean('verbose').pipe(
|
|
148
|
+
Flag.withAlias('v'),
|
|
149
|
+
Flag.withDefault(false)
|
|
150
|
+
); // switch semantics: omission is false
|
|
151
|
+
|
|
152
|
+
// Hidden from help, completions, and typo suggestions, but still parsed
|
|
153
|
+
Flag.boolean('experimental-foo').pipe(
|
|
154
|
+
Flag.withHidden,
|
|
155
|
+
Flag.withDefault(false)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Description
|
|
159
|
+
Flag.string('config').pipe(Flag.withDescription('Path to config file'));
|
|
160
|
+
|
|
161
|
+
// Default value (makes flag optional with fallback)
|
|
162
|
+
Flag.integer('port').pipe(Flag.withDefault(3000));
|
|
163
|
+
|
|
164
|
+
// Optional (returns Option<T>)
|
|
165
|
+
Flag.string('token').pipe(Flag.optional);
|
|
166
|
+
|
|
167
|
+
// Custom metavar for help
|
|
168
|
+
Flag.string('db-url').pipe(Flag.withMetavar('URL')); // --db-url URL
|
|
169
|
+
|
|
170
|
+
// Repetition
|
|
171
|
+
Flag.string('tag').pipe(Flag.atLeast(1)); // --tag a --tag b
|
|
172
|
+
Flag.string('warning').pipe(Flag.atMost(3));
|
|
173
|
+
Flag.string('host').pipe(Flag.between(1, 3));
|
|
174
|
+
|
|
175
|
+
// Transform
|
|
176
|
+
Flag.integer('port').pipe(Flag.map((p) => `http://localhost:${p}`));
|
|
177
|
+
|
|
178
|
+
// Validate with Schema
|
|
179
|
+
Flag.string('email').pipe(Flag.withSchema(EmailSchema));
|
|
180
|
+
|
|
181
|
+
// Filter
|
|
182
|
+
Flag.integer('port').pipe(
|
|
183
|
+
Flag.filter(
|
|
184
|
+
(p) => p >= 1 && p <= 65535,
|
|
185
|
+
(p) => `Port ${p} out of range`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Fallback from env config
|
|
190
|
+
Flag.boolean('verbose').pipe(
|
|
191
|
+
Flag.withFallbackConfig(Config.boolean('VERBOSE'))
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Fallback interactive prompt
|
|
195
|
+
Flag.string('name').pipe(
|
|
196
|
+
Flag.withFallbackPrompt(Prompt.text({ message: 'Name' }))
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Hidden flags parse normally, but generated help, shell completions, and typo suggestions omit them.
|
|
201
|
+
|
|
202
|
+
Bare boolean flags are required. `--verbose` produces `true`, `--no-verbose` produces `false`, and omission produces `CliError.MissingOption`. Add `Flag.withDefault(false)` for ordinary opt-in switch behavior, or use `Flag.optional` / a config or prompt fallback when absence has separate meaning.
|
|
203
|
+
|
|
204
|
+
### Prompt Defaults and Prefixes
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { Prompt } from 'effect/unstable/cli';
|
|
208
|
+
|
|
209
|
+
Prompt.integer({ message: 'Count', default: 42 });
|
|
210
|
+
Prompt.file({ message: 'Pick file', default: '/workspace/config.json' });
|
|
211
|
+
|
|
212
|
+
// The default prefix is "?"; an empty string omits it.
|
|
213
|
+
Prompt.text({ message: 'Name', prefix: '>' });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Integer prompt defaults are editable and Enter submits the default if unchanged. `Prompt.file` resolves/selects the default as the initial path.
|
|
217
|
+
|
|
218
|
+
## Commands
|
|
219
|
+
|
|
220
|
+
### Creating Commands
|
|
221
|
+
|
|
222
|
+
`Command.make` accepts a name, optional config object, and optional handler:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { Console, Effect } from 'effect';
|
|
226
|
+
import { Argument, Command, Flag } from 'effect/unstable/cli';
|
|
227
|
+
|
|
228
|
+
// Simple command (no config, no handler)
|
|
229
|
+
const version = Command.make('version');
|
|
230
|
+
|
|
231
|
+
// Command with config (no handler yet)
|
|
232
|
+
const deploy = Command.make('deploy', {
|
|
233
|
+
env: Flag.string('env'),
|
|
234
|
+
force: Flag.boolean('force').pipe(Flag.withDefault(false)),
|
|
235
|
+
files: Argument.string('files').pipe(Argument.variadic)
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Command with config and inline handler
|
|
239
|
+
const greet = Command.make(
|
|
240
|
+
'greet',
|
|
241
|
+
{
|
|
242
|
+
name: Argument.string('name').pipe(
|
|
243
|
+
Argument.withDescription('Person to greet')
|
|
244
|
+
),
|
|
245
|
+
times: Flag.integer('times').pipe(Flag.withDefault(1))
|
|
246
|
+
},
|
|
247
|
+
Effect.fn(function* ({ name, times }) {
|
|
248
|
+
for (let i = 0; i < times; i++) {
|
|
249
|
+
yield* Console.log(`Hello, ${name}!`);
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Handler Pattern
|
|
256
|
+
|
|
257
|
+
Handlers use `Effect.fn` with a generator that destructures the config:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const cmd = Command.make(
|
|
261
|
+
'deploy',
|
|
262
|
+
{
|
|
263
|
+
env: Flag.choice('env', ['dev', 'staging', 'prod']),
|
|
264
|
+
dryRun: Flag.boolean('dry-run').pipe(Flag.withDefault(false))
|
|
265
|
+
},
|
|
266
|
+
Effect.fn(function* ({ env, dryRun }) {
|
|
267
|
+
if (dryRun) {
|
|
268
|
+
yield* Console.log(`Would deploy to ${env}`);
|
|
269
|
+
} else {
|
|
270
|
+
yield* Console.log(`Deploying to ${env}...`);
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Alternatively, add a handler later with `Command.withHandler`:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const cmd = Command.make('greet', {
|
|
280
|
+
name: Flag.string('name')
|
|
281
|
+
}).pipe(Command.withHandler(({ name }) => Console.log(`Hello, ${name}!`)));
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Command Metadata
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
Command.make('deploy', config, handler).pipe(
|
|
288
|
+
Command.withDescription('Deploy the application'),
|
|
289
|
+
Command.withShortDescription('Deploy app'), // used in subcommand listings
|
|
290
|
+
Command.withAlias('d'), // alternate name
|
|
291
|
+
Command.unlisted, // omit internal/experimental subcommands from discovery
|
|
292
|
+
Command.withExamples([
|
|
293
|
+
{
|
|
294
|
+
command: 'myapp deploy --env prod',
|
|
295
|
+
description: 'Deploy to production'
|
|
296
|
+
},
|
|
297
|
+
{ command: 'myapp deploy --env dev --dry-run', description: 'Dry run' }
|
|
298
|
+
])
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`Command.unlisted` keeps a subcommand invocable by exact name while omitting it from parent help output, shell completions, and "did you mean?" suggestions. In Effect v4, this replaces `Command.withHidden`; the command metadata property is `unlisted`. `Flag.withHidden` remains the correct combinator for flags.
|
|
303
|
+
|
|
304
|
+
### Nested Config
|
|
305
|
+
|
|
306
|
+
Config objects can be nested for organization:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const deploy = Command.make('deploy', {
|
|
310
|
+
environment: Flag.string('env'),
|
|
311
|
+
server: {
|
|
312
|
+
host: Flag.string('host').pipe(Flag.withDefault('localhost')),
|
|
313
|
+
port: Flag.integer('port').pipe(Flag.withDefault(3000))
|
|
314
|
+
},
|
|
315
|
+
files: Argument.string('files').pipe(Argument.variadic)
|
|
316
|
+
});
|
|
317
|
+
// Handler receives: { environment: string, server: { host: string, port: number }, files: ReadonlyArray<string> }
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Subcommands
|
|
321
|
+
|
|
322
|
+
### Basic Subcommands
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const app = Command.make('app');
|
|
326
|
+
|
|
327
|
+
const init = Command.make(
|
|
328
|
+
'init',
|
|
329
|
+
{},
|
|
330
|
+
Effect.fn(function* () {
|
|
331
|
+
yield* Console.log('Initializing...');
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const build = Command.make(
|
|
336
|
+
'build',
|
|
337
|
+
{
|
|
338
|
+
target: Flag.choice('target', ['web', 'node'])
|
|
339
|
+
},
|
|
340
|
+
Effect.fn(function* ({ target }) {
|
|
341
|
+
yield* Console.log(`Building for ${target}`);
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
app.pipe(
|
|
346
|
+
Command.withSubcommands([init, build]),
|
|
347
|
+
Command.run({ version: '1.0.0' }),
|
|
348
|
+
Effect.provide(NodeServices.layer),
|
|
349
|
+
NodeRuntime.runMain
|
|
350
|
+
);
|
|
351
|
+
// Usage: app init | app build --target web
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Shared Parent Flags
|
|
355
|
+
|
|
356
|
+
Use `Command.withSharedFlags` to define flags on a parent that are available to all subcommands. Subcommands access parent config by yielding the parent command:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const tasks = Command.make('tasks').pipe(
|
|
360
|
+
Command.withSharedFlags({
|
|
361
|
+
workspace: Flag.string('workspace').pipe(
|
|
362
|
+
Flag.withAlias('w'),
|
|
363
|
+
Flag.withDefault('personal')
|
|
364
|
+
),
|
|
365
|
+
verbose: Flag.boolean('verbose').pipe(
|
|
366
|
+
Flag.withAlias('v'),
|
|
367
|
+
Flag.withDefault(false)
|
|
368
|
+
)
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const create = Command.make(
|
|
373
|
+
'create',
|
|
374
|
+
{
|
|
375
|
+
title: Argument.string('title'),
|
|
376
|
+
priority: Flag.choice('priority', ['low', 'normal', 'high']).pipe(
|
|
377
|
+
Flag.withDefault('normal')
|
|
378
|
+
)
|
|
379
|
+
},
|
|
380
|
+
Effect.fn(function* ({ title, priority }) {
|
|
381
|
+
// Access parent config by yielding the parent command
|
|
382
|
+
const root = yield* tasks;
|
|
383
|
+
if (root.verbose) {
|
|
384
|
+
yield* Console.log(`workspace=${root.workspace} action=create`);
|
|
385
|
+
}
|
|
386
|
+
yield* Console.log(
|
|
387
|
+
`Created "${title}" in ${root.workspace} with ${priority} priority`
|
|
388
|
+
);
|
|
389
|
+
})
|
|
390
|
+
).pipe(
|
|
391
|
+
Command.withDescription('Create a task'),
|
|
392
|
+
Command.withExamples([
|
|
393
|
+
{
|
|
394
|
+
command: 'tasks create "Ship 4.0" --priority high',
|
|
395
|
+
description: 'Create a high-priority task'
|
|
396
|
+
}
|
|
397
|
+
])
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const list = Command.make(
|
|
401
|
+
'list',
|
|
402
|
+
{
|
|
403
|
+
status: Flag.choice('status', ['open', 'done', 'all']).pipe(
|
|
404
|
+
Flag.withDefault('open')
|
|
405
|
+
),
|
|
406
|
+
json: Flag.boolean('json').pipe(Flag.withDefault(false))
|
|
407
|
+
},
|
|
408
|
+
Effect.fn(function* ({ status, json }) {
|
|
409
|
+
const root = yield* tasks;
|
|
410
|
+
if (json) {
|
|
411
|
+
yield* Console.log(
|
|
412
|
+
JSON.stringify({ workspace: root.workspace, status }, null, 2)
|
|
413
|
+
);
|
|
414
|
+
} else {
|
|
415
|
+
yield* Console.log(`Listing ${status} tasks in ${root.workspace}`);
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
).pipe(Command.withDescription('List tasks'), Command.withAlias('ls'));
|
|
419
|
+
|
|
420
|
+
tasks.pipe(
|
|
421
|
+
Command.withSubcommands([create, list]),
|
|
422
|
+
Command.run({ version: '1.0.0' }),
|
|
423
|
+
Effect.provide(NodeServices.layer),
|
|
424
|
+
NodeRuntime.runMain
|
|
425
|
+
);
|
|
426
|
+
// Usage: tasks --workspace team-a list --status open
|
|
427
|
+
// Usage: tasks create "Ship 4.0" --priority high
|
|
428
|
+
// Usage: tasks ls --json
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Grouped Subcommands
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
app.pipe(
|
|
435
|
+
Command.withSubcommands([
|
|
436
|
+
init,
|
|
437
|
+
{ group: 'Development', commands: [build, test] },
|
|
438
|
+
{ group: 'Deployment', commands: [deploy, rollback] }
|
|
439
|
+
])
|
|
440
|
+
);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Dependency Injection
|
|
444
|
+
|
|
445
|
+
### Provide a Layer
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const deploy = Command.make(
|
|
449
|
+
'deploy',
|
|
450
|
+
{
|
|
451
|
+
env: Flag.string('env')
|
|
452
|
+
},
|
|
453
|
+
Effect.fn(function* ({ env }) {
|
|
454
|
+
const fs = yield* FileSystem.FileSystem;
|
|
455
|
+
// ...
|
|
456
|
+
})
|
|
457
|
+
).pipe(Command.provide(FileSystemLive));
|
|
458
|
+
|
|
459
|
+
// Layer can depend on parsed input
|
|
460
|
+
Command.provide((config) =>
|
|
461
|
+
config.env === 'local' ? LocalFsLayer : RemoteFsLayer
|
|
462
|
+
);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Provide a Service
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
Command.provideSync(MyService, makeMyService());
|
|
469
|
+
Command.provideEffect(MyService, Effect.succeed(makeMyService()));
|
|
470
|
+
|
|
471
|
+
// Can depend on parsed input
|
|
472
|
+
Command.provideSync(MyService, (config) => makeMyService(config.env));
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Running Commands
|
|
476
|
+
|
|
477
|
+
`Command.run` is a **pipeable combinator** that reads args from `Stdio`. The resulting effect requires `FileSystem`, `Path`, `Terminal`, `Stdio`, and `ChildProcessSpawner`; provide platform services and execute with the runtime:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { NodeRuntime, NodeServices } from '@effect/platform-node';
|
|
481
|
+
import { Effect } from 'effect';
|
|
482
|
+
import { Command, Flag } from 'effect/unstable/cli';
|
|
483
|
+
|
|
484
|
+
const myCommand = Command.make(
|
|
485
|
+
'myapp',
|
|
486
|
+
{
|
|
487
|
+
name: Flag.string('name')
|
|
488
|
+
},
|
|
489
|
+
Effect.fn(function* ({ name }) {
|
|
490
|
+
yield* Console.log(`Hello, ${name}!`);
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// Entry point pattern
|
|
495
|
+
myCommand.pipe(
|
|
496
|
+
Command.run({ version: '1.0.0' }),
|
|
497
|
+
Effect.provide(NodeServices.layer),
|
|
498
|
+
NodeRuntime.runMain
|
|
499
|
+
);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Auto-generates `--help` and `--version` flags.
|
|
503
|
+
|
|
504
|
+
Built-in/global flags (`--help`, `--version`, `--completions`, `--log-level`, plus custom globals from `Command.withGlobalFlags`) are parsed for the active command path. A local flag on the selected command can intentionally reuse/override a global flag name or alias. Shared parent flags from `Command.withSharedFlags` remain command context and may be accepted before or after a subcommand.
|
|
505
|
+
|
|
506
|
+
### Testing with Explicit Args
|
|
507
|
+
|
|
508
|
+
Use `Command.runWith` to pass args directly (useful in tests):
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
const run = Command.runWith(myCommand, { version: '1.0.0' });
|
|
512
|
+
// run(["--name", "Alice"]) => Effect<void, ...>
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Key Patterns
|
|
516
|
+
|
|
517
|
+
1. **`Argument` = positional, `Flag` = named** — No `Argument.boolean`; use `Flag.boolean` for toggles, and add `Flag.withDefault(false)` when omission should mean `false`
|
|
518
|
+
2. **Handlers use `Effect.fn`** — `Effect.fn(function*({ ...config }) { ... })`
|
|
519
|
+
3. **Parent access via yield** — `const root = yield* parentCommand` inside subcommand handlers
|
|
520
|
+
4. **Shared flags** — `Command.withSharedFlags` on parent; only flags allowed (no arguments)
|
|
521
|
+
5. **Pipeable `Command.run`** — `command.pipe(Command.run({version}), Effect.provide(NodeServices.layer), NodeRuntime.runMain)`
|
|
522
|
+
6. **Platform services required** — `Command.run` requires `FileSystem`, `Path`, `Terminal`, `Stdio`, and `ChildProcessSpawner`; provide via `NodeServices.layer` / `BunServices.layer`
|
|
523
|
+
7. **All combinators are dual** — Work both as `pipe(Flag.withAlias("v"))` and `Flag.withAlias(flag, "v")`
|