@gjermundgaraba/effect-actions 0.8.0 → 0.10.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 (68) hide show
  1. package/README.md +57 -50
  2. package/dist/Action.d.mts +285 -79
  3. package/dist/Action.mjs +124 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +108 -81
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +82 -57
  15. package/dist/Testing.mjs +89 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +11 -0
  34. package/dist/internal/mcp.mjs +52 -0
  35. package/dist/internal/memory.d.mts +6 -0
  36. package/dist/internal/memory.mjs +38 -0
  37. package/dist/internal/refusal.mjs +67 -0
  38. package/dist/internal/schema-errors.mjs +20 -0
  39. package/dist/internal/tools.mjs +27 -57
  40. package/docs/Action.md +401 -73
  41. package/docs/ActionCli.md +280 -51
  42. package/docs/ActionHttp.md +266 -82
  43. package/docs/ActionMcp.md +149 -104
  44. package/docs/ActionToolkit.md +97 -35
  45. package/docs/Authentication.md +369 -62
  46. package/docs/CONTEXT.md +23 -36
  47. package/docs/README.md +29 -73
  48. package/docs/Testing.md +154 -77
  49. package/docs/guarantees.md +81 -43
  50. package/docs/setup.md +90 -27
  51. package/package.json +9 -17
  52. package/dist/ActionCatalog.d.mts +0 -30
  53. package/dist/ActionCatalog.mjs +0 -39
  54. package/dist/ActionCliClient.d.mts +0 -48
  55. package/dist/ActionCliClient.mjs +0 -43
  56. package/dist/ActionGroup.d.mts +0 -44
  57. package/dist/ActionGroup.mjs +0 -35
  58. package/dist/ActionHttpClient.d.mts +0 -33
  59. package/dist/ActionHttpClient.mjs +0 -16
  60. package/dist/TestingClient.d.mts +0 -17
  61. package/dist/TestingClient.mjs +0 -22
  62. package/dist/internal/fetchClient.d.mts +0 -6
  63. package/dist/internal/fetchClient.mjs +0 -8
  64. package/dist/internal/tools.d.mts +0 -22
  65. package/docs/ActionCatalog.md +0 -54
  66. package/docs/ActionCliClient.md +0 -63
  67. package/docs/ActionGroup.md +0 -128
  68. package/docs/ActionHttpClient.md +0 -80
package/docs/ActionCli.md CHANGED
@@ -1,78 +1,307 @@
1
1
  # ActionCli
2
2
 
3
- Native Effect CLI commands that run handlers in-process. The host provides every service;
4
- nothing is fetched over HTTP. Any action runs locally, whether or not HTTP or MCP serves it.
3
+ Native Effect CLI commands for actions, with flags derived from each action's input. `command`
4
+ and `make` take implementations: a command runs the handler in process, with every service the
5
+ host's; any implemented action runs locally, whether or not HTTP or MCP serves it.
6
+ `remoteCommand` and `remote` take an HTTP binding: a command calls the action over HTTP through
7
+ its `ActionHttp.client` method instead, and runs nothing locally.
5
8
 
6
9
  ## API
7
10
 
8
11
  Import `@gjermundgaraba/effect-actions/ActionCli`.
9
12
 
10
- | API | Purpose |
11
- | ------------------------------ | ------------------------------------------------- |
12
- | `command(app, name, options?)` | One selected action as a native Effect `Command`. |
13
- | `group(app, options?)` | Every action under a group command. |
14
- | `Options`, `GroupOptions` | Configuration for single and aggregate commands. |
13
+ | API | Purpose |
14
+ | -------------------------------------------- | ------------------------------------------------------------------- |
15
+ | `command(implementations, action, options?)` | One action, selected by its contract, as a native Effect `Command`. |
16
+ | `make(implementations, options)` | Every implemented action as a subcommand of one aggregate command. |
17
+ | `remoteCommand(binding, action, options?)` | One action of an HTTP binding, called over HTTP. |
18
+ | `remote(binding, options)` | Every action of the binding as a subcommand, called over HTTP. |
19
+ | `logToStderr` | Applied last before `runMain`: stdout carries only results. |
15
20
 
16
- | Option | Meaning |
17
- | --------------------- | ------------------------------------------------------------------------------------------------------------ |
18
- | `name` | Override the command/group name. |
19
- | `before` | Hook receiving the selected `Action.Any`; its failures and services join the command's channels. |
20
- | `render` | Single command only: decoded success to human-readable string; adds `--json`. |
21
- | `parameters`, `input` | Single command only; supply together. Native flag/argument config and parsed-config-to-encoded-JSON mapping. |
21
+ | Option | Meaning |
22
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
23
+ | `command`, `remoteCommand`: `name` | Override the command name (default: the action name in kebab case). |
24
+ | `command`, `remoteCommand`: `render` | Decoded success to human-readable string; adds `--json`. |
25
+ | `command`, `remoteCommand`: `positional` | Input fields taken as positional arguments instead of flags, in this order. |
26
+ | `command`, `remoteCommand`: `aliases` | A short name per field's flag: `{ limit: "n" }` takes `-n 5` beside `--limit 5`. |
27
+ | `make`, `remote`: `name` | The aggregate's name, required. |
28
+ | `make`, `remote`: `actions` | The actions that are subcommands, among the implementations' or the binding's; defaults to every one. |
29
+ | `make`, `remote`: `commands` | Each subcommand's `command` options, keyed by action name: `{ readFile: { positional: ["path"], render } }`. |
30
+ | `remoteCommand`, `remote`: `client` | The `ActionHttp.client` options, `baseUrl` and `transformClient`, for this command's or aggregate's client. |
22
31
 
23
- Without `parameters`/`input`, a command accepts `--input` and `--input-file`. Commands retain
24
- handler, builder and hook requirements/failures, plus codec failures. The invocation owns its
25
- scope. Success output is encoded; failures remain failures of the command Effect.
32
+ Exported types: `Options<A>` of `make` and `RemoteOptions<A>` of `remote`, `A` the union of its actions, such as `typeof GetUser | typeof RenameUser`; `CommandOptions<typeof Action>` of `command` and `RemoteCommandOptions<typeof Action>` of `remoteCommand`, the remote ones adding `client`, an `ActionHttp.ClientOptions`; and `UserError<E>`, a type only: what a command fails with when its action fails, Effect CLI's `CliError.UserError` whose `cause`, and `reason`, is the failure `E`. A command over HTTP calls through the host's `HttpClient`, configured by its `client` options: where it sends and any credentials.
33
+
34
+ Flags come from the action's input. A struct or class input gets one flag per top-level
35
+ field, named in kebab case (`tenantId` is `--tenant-id`, `getHTTPUser` is `get-http-user`, `_id` is `--id`), parsing the field's encoded JSON
36
+ value:
37
+
38
+ | Encoded field | Flag |
39
+ | ---------------------------------------- | --------------------------------------------------------------------- |
40
+ | string or template literal | `--name <string>`; the action's schema checks a template |
41
+ | boolean | `--on`, a switch; omitted is `false` for a required field |
42
+ | union of string literals, or string enum | `--kind <choice>`, one of the values |
43
+ | array of strings or numbers, or choices | `--tag <value>`, repeated: one element per occurrence; `[]` adds none |
44
+ | anything else, numbers included | `--owner <value>`: JSON the field accepts, or the text |
45
+
46
+ A suspended input or field, as a recursive schema is written, counts as the schema it stands
47
+ for, its description included.
48
+
49
+ An input that is not a struct of named fields (a union, a record, a scalar) gets one
50
+ `--input <value>` flag carrying the whole encoded input. Left off, the input is `{}`, which the
51
+ action's schema decodes when the command runs. A schema accepting `{}`, including a union
52
+ with such a member, succeeds; otherwise the command fails with `InvalidInput`. An action without input gets no flags.
53
+ A repeated flag reads each occurrence as one element, as the flag of that element would:
54
+ `--provider exa --provider hn` for `Schema.Array(Schema.Literals(["exa", "hn"]))`. Given none, a
55
+ required field is `[]` and an optional one is left out; a `Schema.NonEmptyArray` field's flag is
56
+ required once. An occurrence of `[]` adds no element, and is one of a choice's values, so
57
+ `--tags '[]'` alone sends `[]`, clearing an optional field the flag would otherwise leave out; an
58
+ element whose text is `[]` cannot be sent. Any other array, such as one of objects, takes JSON,
59
+ `--points '[{"x":1}]'`.
60
+ A value flag parses its text as JSON when the field's encoding accepts that kind of value
61
+ (`--count 2`, `--owner '{"id":"x"}'`), or else keeps the text (`--limit auto`, `--scale Infinity`
62
+ for `Schema.Number`, `--mode true` for `"auto" | string`); the action's schema decodes either.
63
+ Only the kind decides: JSON breaking a rule, such as `--width 0` where it must be positive, stays
64
+ JSON, and the schema reports the rule and its path.
65
+ Choices may be nested unions: `Schema.Union([Schema.Literals(["a", "b"]), Schema.Literal("c")])`
66
+ is one choice of three.
67
+ A field's description is its flag's help text, whatever its encoding, except in a struct that
68
+ `Schema.encodeKeys` renames: its flags go by the encoded names and are described by the encoded
69
+ fields, so a transformed field there has no help text, as JSON Schema drops it. An optional field's
70
+ flag is optional and takes its value without the `null` that `Schema.optional` encodes
71
+ (`--name x` for `Schema.optional(Schema.String)`); a required `Schema.NullOr` field takes a value (`--name x`, `--name null`).
72
+ An optional field whose own schema encodes `null` takes it too: `--note null` is `Option.none()`
73
+ for `Schema.optionalKey(Schema.OptionFromNullOr(Schema.String))`, renamed or not.
74
+
75
+ A field listed in `positional` is an argument instead of a flag, parsed as its flag would
76
+ be, a boolean taking `true` or `false`: `command(implementations, Inspect, { positional: ["path"] })`
77
+ reads `inspect README.md --lines 10`. Arguments are read in the listed order, not the
78
+ input's. An optional field's argument is optional. An array whose flag repeats is a repeated
79
+ argument, one element per value, as its flag takes them, `[]` included: `remove a.txt b.txt` for
80
+ `{ positional: ["paths"] }`. It takes every value left, so it is listed last. A JSON or text
81
+ argument is shown in help as `value`, as its flag is.
82
+ `aliases` gives a field's flag a short name too, as native `Param.withAlias` does:
83
+ `{ aliases: { limit: "n" } }` takes `-n 5` beside `--limit 5`.
84
+
85
+ A local command requires what its handler, builder and authorizer require, and a protected
86
+ action's identity, and the invocation owns its scope. Its `UserError`'s cause is what the action,
87
+ its authorizer or the builder fails with, or a
88
+ built-in error. A command over HTTP requires an `HttpClient` supplied by the host, and its
89
+ cause is exactly what the action's `ActionHttp.client` method fails with. A command prints
90
+ only the encoded success on stdout; `Command.run` prints a failure on stderr.
26
91
 
27
92
  ## Canonical
28
93
 
29
- ```ts
30
- import { Console, Effect, Logger } from "effect";
31
- import { Command, Flag } from "effect/cli";
94
+ ```ts example=cli.ts
95
+ import { Effect } from "effect";
96
+ import { Command } from "effect/cli";
32
97
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
33
98
  import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
34
- import { actors, authorize, CurrentActor } from "./auth.js";
35
- import { UserApp } from "./handlers.js";
99
+ import { actors, CurrentActor } from "./authorization.js";
100
+ import { userActions } from "./handlers.js";
36
101
  import { Users } from "./users.js";
37
102
 
38
- // The CLI binds the same hook as the servers; a local caller is not trusted more.
39
- const command = ActionCli.command(UserApp, "double", {
40
- parameters: { value: Flag.String("value") },
41
- input: ({ value }) => ({ value }),
42
- before: authorize,
103
+ // `users get-user --id 1`: a subcommand per action, a flag per input field. The
104
+ // implementation's `authorize` runs here as on the servers; a local caller is not trusted more.
105
+ const cli = ActionCli.make(userActions, { name: "users" }).pipe(
106
+ // Services go on the command: built when an action runs, never for `--help` or a
107
+ // mistyped flag.
108
+ Command.provide(Users.layerMemory),
109
+ // No remote caller to authenticate: the host supplies the identity the contracts declare.
110
+ Command.provideSync(CurrentActor, actors.alice),
111
+ );
112
+
113
+ // Effect's own runner: the result goes to stdout, and a failure to stderr as the JSON HTTP
114
+ // sends, such as `{"_tag":"UserNotFound","id":"9"}`, exiting 1. `logToStderr`, applied last,
115
+ // sends every other report and log there too, so scripts read stdout alone.
116
+ Command.run(cli, { version: "0.1.0" }).pipe(
117
+ Effect.provide(NodeServices.layer),
118
+ ActionCli.logToStderr,
119
+ NodeRuntime.runMain,
120
+ );
121
+ ```
122
+
123
+ ### Over HTTP
124
+
125
+ ```ts example=cli-remote.ts
126
+ import { Command } from "effect/cli";
127
+ import { Effect } from "effect";
128
+ import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
129
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
130
+ import { Http } from "./binding.js";
131
+ import { Status } from "./contracts.js";
132
+
133
+ // From a binding rather than implementations, the command calls the server instead. Its
134
+ // client options are the connection, as `ActionHttp.client` takes them: where it sends, and
135
+ // any credentials, which reach no other request the program makes.
136
+ const command = ActionCli.remoteCommand(Http, Status, {
137
+ client: { baseUrl: "http://127.0.0.1:3000" },
43
138
  });
44
139
 
45
- Command.runWith(command, { version: "0.1.0" })(process.argv.slice(2)).pipe(
46
- Effect.tapCause((cause) => Console.error(cause)),
47
- Effect.provideService(Logger.LogToStderr, true),
48
- // The hook runs here too, so a local caller supplies an identity for it
49
- // exactly as HTTP middleware does for a request.
50
- Effect.provideService(CurrentActor, actors.alice),
51
- Effect.provide(Users.layerMemory),
140
+ Command.run(command, { version: "0.1.0" }).pipe(
141
+ Effect.provide(NodeHttpClient.layerUndici),
52
142
  Effect.provide(NodeServices.layer),
53
- NodeRuntime.runMain({ disableErrorReporting: true }),
143
+ NodeRuntime.runMain,
144
+ );
145
+ ```
146
+
147
+ A static token goes in the same options, `transformClient:
148
+ HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. A URL or credentials read when a
149
+ command runs, as from `Config`, configure the client the command is provided instead. An
150
+ aggregate nests in the host's own tree like any native command, and what it is provided
151
+ reaches its own commands alone:
152
+
153
+ ```ts
154
+ const api = ActionCli.remote(Http, { name: "api" }).pipe(
155
+ Command.provideEffect(
156
+ HttpClient.HttpClient,
157
+ Effect.gen(function* () {
158
+ const url = yield* Config.String("ACME_URL");
159
+ const token = yield* Config.Redacted("ACME_TOKEN");
160
+
161
+ return HttpClient.mapRequest(
162
+ yield* HttpClient.HttpClient,
163
+ flow(HttpClientRequest.prependUrl(url), HttpClientRequest.bearerToken(token)),
164
+ );
165
+ }),
166
+ ),
167
+ );
168
+
169
+ // `acme api get-user --id 1` sends the token; the host's own `login` keeps its plain client.
170
+ const cli = Command.make("acme").pipe(Command.withSubcommands([api, login]));
171
+ ```
172
+
173
+ ### Trusted callers
174
+
175
+ An operator's command beside remote callers, from one implementation and one `authorize`. The
176
+ identity is typed in two parts: what a verifier returns, and what only the host supplies, so no
177
+ verified token, and no claim mapped onto a role, names the trusted operator. The host provides
178
+ it on its own command, where the same `authorize` admits it.
179
+
180
+ ```ts example=cli-admin.ts
181
+ import { Context, Effect, Layer, Redacted } from "effect";
182
+ import { Command } from "effect/cli";
183
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
184
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
185
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
186
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
187
+
188
+ /** What a verified token can name. */
189
+ export interface RemoteOperator {
190
+ readonly id: string;
191
+ readonly role: "viewer" | "editor";
192
+ }
193
+
194
+ /** What only a host supplies: no verifier returns one. */
195
+ export interface TrustedOperator {
196
+ readonly id: string;
197
+ readonly role: "trusted";
198
+ }
199
+
200
+ export class Operator extends Context.Service<Operator, RemoteOperator | TrustedOperator>()(
201
+ "example/Operator",
202
+ ) {}
203
+
204
+ export const Purge = Action.make("purge", {
205
+ description: "Purge the cache.",
206
+ readOnly: false,
207
+ caller: Operator,
208
+ });
209
+
210
+ // One rule for every caller: a trusted operator passes, a remote one needs the editor role.
211
+ export const cache = Action.implement(Purge, () => Effect.log("Purged."), {
212
+ authorize: (action) =>
213
+ Effect.gen(function* () {
214
+ const operator = yield* Operator;
215
+
216
+ if (operator.role === "trusted") return;
217
+
218
+ if (!action.readOnly && operator.role !== "editor") {
219
+ return yield* new Action.Forbidden({ message: "Requires the editor role." });
220
+ }
221
+ }),
222
+ });
223
+
224
+ const OperatorLogin = Authentication.make("example.OperatorLogin", Operator);
225
+
226
+ export const Http = ActionHttp.make([Purge], { authentication: OperatorLogin });
227
+
228
+ // DEMO ONLY: a token is a role. Typed as the remote subset, so no token, and no claim mapped
229
+ // onto `role`, can name the trusted operator.
230
+ const verify = (
231
+ token: Redacted.Redacted<string>,
232
+ ): Effect.Effect<RemoteOperator, Action.Unauthenticated> => {
233
+ const role = Redacted.value(token);
234
+
235
+ return role === "viewer" || role === "editor"
236
+ ? Effect.succeed({ id: role, role })
237
+ : Effect.fail(new Action.Unauthenticated({ message: "Unknown demo token." }));
238
+ };
239
+
240
+ export const routes = ActionHttp.layer(Http, cache).pipe(
241
+ Layer.provide(Authentication.layer(OperatorLogin, verify)),
242
+ );
243
+
244
+ const operator: TrustedOperator = { id: "ops", role: "trusted" };
245
+
246
+ // The host's own command, `ops purge`: the same implementation and rule, run as the trusted
247
+ // operator the host supplies. Nothing remote can reach it.
248
+ export const cli = ActionCli.make(cache, { name: "ops" }).pipe(
249
+ Command.provideSync(Operator, operator),
54
250
  );
55
251
  ```
56
252
 
57
253
  ## Rules
58
254
 
59
- - `command(app, name)` selects one action by name from the bound group; the name is checked at the type level. `group(app)` puts every action under the group command, including actions with `mcp: false` and groups no HTTP binding serves.
60
- - Default syntax is `--input '<json>'` or `--input-file <path>` carrying the entire encoded input: nested objects, arrays, scalars. The file is read and JSON-parsed by the native `Flag.FileSchema`. Both are decoded by the native parser; if both are given, the file is used. Omitting both decodes `{}` separately on every invocation; result identity follows the codec's behavior. An action that requires input then fails with a `SchemaError`, exactly as a `parameters` command's mapped input does.
61
- - `parameters` and `input` are supplied together or not at all. With them, the command has explicit native flags and arguments and neither `--input` nor `--input-file`, so its syntax does not change when the schema changes.
62
- - `input(parsed)` returns encoded JSON. The action schema decodes it before dispatch; a mismatch is a `SchemaError` at runtime. Action fields are never turned into flags automatically.
63
- - Native `Flag` and `Argument` own names, aliases, ordering, defaults, and optionality. Use `Flag.optional` when omission must stay distinct from a default, and map the `Option` to a present or omitted field.
64
- - Output is validated and encoded before printing. Default output is JSON. `render(decoded)` gives human output and adds a `--json` flag to that command, which selects JSON again. Rendering cannot bypass validation.
65
- - `--json` is a regular flag of the rendered command only, placed after the command name on a `group` path. A command without `render` has no such flag: its output is JSON already. Nothing is declared tree-wide, so a host CLI may declare its own `--json`, global or not. A rendered `parameters` command reserves the flag name; a payload field named `json` is ordinary data.
66
- - Each invocation acquires the implementation in a scope and releases it after the call. Domain services and authority come from the host's provided layers. There is no HTTP fallback.
67
- - `before` runs before the selected handler, on `command` and on every subcommand of `group`. Its failure joins the command's error channel and its services are the caller's to provide, so a local CLI supplies the identity the hook reads exactly as HTTP middleware does. A CLI is not a trusted bypass.
68
- - The CLI declares no surface `errors`: it does not serialize failures, so a refusal is simply a typed failure of the command effect. Success values are still validated and encoded for output. The native parser also decodes `--input` and `--input-file` before the command runs, so invalid input skips the hook and handler.
69
- - Group commands use the default JSON syntax. For a custom tree, compose individual `command` results with native `Command` combinators.
255
+ - `command(implementations, action)` selects the implementation of `action` among `implementations` (one implementation or a list) by contract identity, not by name: two contracts that share a name select their own implementations. The action must be one of the implementations' actions: the types refuse an action of another shape, and the runtime check refuses an equal-looking one.
256
+ - `make(implementations, { name })` puts every implemented action under one command named `name`, or those `actions` lists, each keeping its implementation's authorizer, one subcommand per action, named after it in kebab case (`getUser` is `get-user`). `commands` gives one subcommand the options `command` takes, by its action's name, typed by that action. It takes any action of the implementations, or of the binding for `remote`, so one record serves aggregates of several selections: a command of an action `actions` leaves out is unused. A key no action of them names is refused. That includes actions no HTTP binding or MCP endpoint serves. Subcommand names must be distinct after kebab-casing; two actions whose names collide are refused.
257
+ - Flags are values in their encoded form: `double --value 21` for a `FiniteFromString` field, which is string-encoded; a value flag takes the encoded JSON, or text. The action's schema then decodes the assembled input before dispatch.
258
+ - A field that is required once encoded has a required flag. Omitted, the parser refuses the command with `Missing required flag: --<flag>` and shows its help, and the implementation is not built. A required boolean is the exception: omitted, its switch is `false`.
259
+ - An optional field's flag is optional. Omitting it leaves the field out, including a field with a decoding default. The action's schema then decodes what the flags parsed, so transforms and cross-field rules still apply.
260
+ - `positional` names fields of a struct or class input, and the types offer none for any other input, a union included. Each is listed once, and every required field before any optional one, since a parser reads arguments in order; an array taken as a repeated argument comes last, as it reads every value left. Remote commands take it too, and `aliases`, which names only flagged fields of such an input, each alias once.
261
+ - The flags follow the schema: renaming a field renames its flag. `name`, `positional`, `aliases` and `render` give a command its name, its arguments, its short flags and its output, and keep every rule of this page: prefer a derived command wherever they express the syntax. For a syntax they cannot, such as a flag named apart from its field, or a command calling several actions, build the command with native `Command.make`, `Flag` and `Argument`, and call the actions in it through `Action.client`, acquired in its handler, inside `Effect.scoped`, so each invocation builds and releases it ([Action.md](Action.md#clients)), or over HTTP through the binding's `ActionHttp.client`. Calling a handler directly bypasses decoding and the implementation's authorizer.
262
+ - Such a command is the host's own: through `Action.client` the authorizer runs, but the output, failure and logging rules of this page do not apply. It prints what it prints, its logs go where the host's logger writes, and a failure it leaves as it is is the action's own, not a `UserError`, which `runMain` reports on stdout with a stack and without its fields. Map it to Effect CLI's `CliError.UserError`, `new CliError.UserError({ cause, userMessage })`, for `Command.run` to print `userMessage` on stderr.
263
+ - Output is validated and encoded before printing. Default output is JSON. `render(decoded)` gives human output and adds a `--json` flag to that command, which selects JSON again. An action whose `success` is `Schema.Void` prints nothing by default or with `--json`; a custom `render` can still print human output. Rendering cannot bypass validation: a success its schema does not encode is a defect, as on HTTP, and nothing prints it.
264
+ - `--json` is a regular flag of the rendered command only. A command without `render` has no such flag: its output is JSON already. Nothing is declared tree-wide, so a host CLI may declare its own `--json`, global or not.
265
+ - Each invocation builds the selected implementation's builder, and its authorizer when an Effect builds it, in a scope of its own and releases them after the call. Only that implementation's builder runs: other implementations passed alongside it, on `command` or `make`, are not built. Domain services and authority come from layers the host provides on the command, `Command.provide(layer)`: built when the command runs, before its input is decoded, so input the action's schema refuses is refused after they are built; `--help` and the parser's errors, such as a missing or unknown flag, never build them. `make`'s aggregate run alone builds them before showing its help, since its own handler shows it, and fails instead if one fails. There is no HTTP fallback.
266
+ - A local command is a local surface: the implementation's `authorize` runs before a protected action's handler, on `command` and on every subcommand of `make`, and the host provides the identity a protected action declares on the command, `Command.provideSync(CurrentActor, actor)`, or `Command.provideEffect(CurrentActor, load)` to read it when an action runs. Its type requires it whether or not the handler or the authorizer reads it.
267
+ - A CLI is not a trusted bypass: it runs the same authorizer as every surface, and takes no authorization of its own. A trusted operator is an identity the host supplies on its own command, typed apart from what a verifier returns, which the shared `authorize` admits ([Trusted callers](#trusted-callers)). A verifier typed to return the remote part cannot name it. The library cannot tell which identities are trusted: keep the trusted part out of every verifier's return type.
268
+ - A command writes only its result to stdout: the builder, authorizer and handler, or the client, and the codecs of its input, success and failures, write their Effect logs and Effect `Console` output to stderr, whatever logger prints them. The global `console.log` and other direct writes bypass Effect and still reach stdout: keep them off stdout.
269
+ - When its action fails, a command fails with Effect CLI's `UserError`, typed `UserError<E>`, whose `cause` is that failure: a declared error, a binding's, a built-in one, or a builder's. Its message is the JSON HTTP sends for it ([guarantees.md](guarantees.md#wire-behavior)), such as `{"_tag":"UserNotFound","id":"9"}`. `Command.run` prints it on stderr through Effect's `CliOutput` formatter and marks it reported, so `NodeRuntime.runMain` prints it no more, and the process exits 1, or with the cause's `Runtime.errorExitCode`. A failure no schema encodes, a builder's or the transport's, prints as its tag, or an error's name, and its message, then each cause's, up to one already printed, and never its other fields, a plain object's `name` among them.
270
+ - Every local command may fail with `Action.BuiltIn`, whatever its implementation's authorizer. Input that does not decode is `InvalidInput`, as over HTTP: it skips the authorizer and the handler.
271
+ - After `Command.run` the failure may be any `UserError`, the parser's too, so a host matches the action's by its tag with Effect's own `catchReason`, after `Command.run` has printed it: `Effect.catchReason("UserError", "UserNotFound", (missing) => ...)`, or `Effect.catchReasons("UserError", { UserNotFound, Forbidden })` for several, decides what follows, such as the exit code, not what was printed. A `UserError`'s `reason` is its `cause`; a `UserError` the parser fails with has none, so it passes through. The types take a tag only where every failure of the command is tagged, as Effect's errors are: a builder failing with a plain `Error` leaves `catchReason` no tag to match, so fail with a tagged error. `UserError` is a type only, since `instanceof` would leave its cause `any`. For text of its own, the host provides Effect's formatter, `CliOutput.layer(Object.assign({}, CliOutput.defaultFormatter(), { formatError }))`; `Command.run(cli, { version, renderErrors: false })` has it print every failure itself, the parser's too.
272
+ - `runMain` reports what is not a command's failure, outside the program, on stdout: a defect, such as a handler's bug or a success that does not encode, and a failure of a layer the host provides, on the command or around the run. Those layers log outside the command too, on stdout. A CLI whose stdout feeds scripts applies `logToStderr` last, before `runMain`, as the example does: the default logger of every layer within writes to stderr, and what `runMain` would report is reported on stderr instead, once, a command's failure `Command.run` printed never again. The process exits as `runMain` makes it: 0, the failure's `Runtime.errorExitCode` or 1, or 130 for an interruption. `logToStderr` does not move those layers' `Console` output or a logger writing through `Console.log`, such as `Logger.consoleJson`: log JSON with `Logger.withConsoleError(Logger.formatJson)`. It is the program's last step, so a custom `teardown` or an outer handler of defects sees a defect standing for the report, an `Error` carrying the exit code, whose `cause` is the original `Cause`. An MCP subprocess applies it after `runStdio` the same way ([ActionMcp.md](ActionMcp.md#rules)).
273
+ - Local and remote are separate functions, each with one signature: `command` and `make` take implementations, `remoteCommand` and `remote` a binding. `remoteCommand(binding, action)` takes one of the binding's actions, matched by object identity at runtime, and calls its route, `<prefix>/<action>`, through the action's `ActionHttp.client` method. `remote(binding, { name })` projects every action of the binding, the same tree `make` builds locally, with the same options and `client`.
274
+ - A command over HTTP runs no authorization of its own: the server owns authentication and authorization. The host provides `HttpClient`, and the remote command or aggregate configures its own client with `client`, the options `ActionHttp.client` takes: where it sends, `baseUrl`, and credentials, `transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. One aggregate has one connection: its `commands` take no `client`. `command` and `make` take no `client`: they run in process and connect nowhere. Settings read when a command runs, as from `Config`, configure the client provided on the remote command or aggregate instead, `Command.provideEffect(HttpClient.HttpClient, ...)`, prepending the URL with `HttpClient.mapRequest(HttpClientRequest.prependUrl(url))`. Configured around the whole program, the client would rewrite every request the program makes, an absolute URL too, and send each the credentials. Nothing is inferred from action arguments.
275
+ - Over HTTP, input is decoded by the action schema before dispatch, then passed to the native client at its normal codec boundary. A failure's cause is the client's: the action's and binding's declared errors, the built-in errors ([guarantees.md](guarantees.md#wire-behavior)), `SchemaError` for an answer that does not decode, and `HttpClientError`. Input that does not decode is `InvalidInput`, and sends nothing.
276
+ - For a custom tree, compose individual `command` results with native `Command` combinators (`Command.make(name).pipe(Command.withSubcommands([...]))`). `make`'s aggregate nests in a host's tree the same way, and what it is provided reaches its own subcommands alone.
70
277
 
71
278
  ## Failure modes
72
279
 
73
- - Native `CliError.ShowHelp` containing `InvalidValue` for a supplied `--input` or `--input-file`: the parser rejected its JSON or schema before the command handler ran. Supply encoded input (`"21"` for `FiniteFromString`, not `21`). With neither flag, decoding the default `{}` can fail with `SchemaError`; so can a `parameters` command's mapped input or an invalid success value.
74
- - Command rejects `--input` or `--input-file`: the command was built with `parameters`. Use its flags.
75
- - Type error: `name` is not an action of the group. Check `group.actions` names, not MCP tool names.
76
- - Handler cannot find a service: provide its Layer to the runtime (`Effect.provide`) before `runMain`. The command does not supply services.
77
- - A command requires a request-identity tag no handler yields: the `before` hook yields it. Provide a trusted identity around the invocation; do not remove the authorization hook just to satisfy the service requirement.
78
- - `--json` flag conflict at definition on a rendered `parameters` command: rename the native flag; the config property can keep its name.
280
+ - `Missing required flag: --<flag>` on stderr, after the command's help on stdout, from a native `CliError.ShowHelp` containing `MissingOption`: a required field's flag was not given. Pass it.
281
+ - `InvalidInput` naming an unexpected key: `--input` or a flag's JSON holds a field the schema does not declare, such as a misspelling. Undeclared fields are refused, never dropped.
282
+ - `InvalidInput` from a command whose input is not a struct, run without `--input`: the schema rejects the default `{}`. Pass `--input` with a value the schema accepts.
283
+ - `InvalidInput` for a value that looks right: the flag takes the encoded value, such as `"21"` for `FiniteFromString`. For `--input` or a value flag, the JSON or text may not be the field's encoding; malformed JSON is taken as text. Quote a string that reads as JSON: `--id '"123"'` for a `String | Number` field.
284
+ - `Invalid value for flag --<flag>: "<value>". Expected: ...`, or `Missing value for flag --<flag>`, from a native `CliError.ShowHelp` containing `InvalidValue`: the parser rejected a flag's text before the command ran: a choice outside its values, or none.
285
+ - `Missing required argument: <field>`, from a native `CliError.ShowHelp` containing `MissingArgument`: a required positional argument was not given. A positional field has no flag, so `--<field>` does not supply it.
286
+ - Thrown by `command` when the command is built: `Duplicate positional argument: <field>` (listed twice), `Required positional argument after an optional one: <field>` (reorder the list, or make the earlier field required), or `Repeated positional argument before another one: <field>` (an array taken as an argument reads every value left: list it last).
287
+ - Thrown by `command` when the command is built: `Not a flag's input field: <field>` (an alias names a field that is positional, or no field), or `Aliases need named input fields: <fields>` (the input is not a struct).
288
+ - Also thrown by `command`, and refused by the types first: `Not an input field: <field>`, or `Positional arguments need named input fields` for an input that is not a struct.
289
+ - `Duplicate flag: --<name>, claimed by ...` thrown by `command` or `make`: two flags of one command share a name. Examples are two input fields with the same kebab-case name (`userId`, `user_id`), a `json` field beside `render`'s `--json`, or an alias another flag's name or alias takes (`aliases: { limit: "all" }` beside an `all` field). Rename the field or the alias, or drop `render`.
290
+ - A field named like a global flag (`help`, `version`, `log-level`) is not a clash: its flag shadows the global one on that command.
291
+ - Type error at `command`, or `Action "x" has no implementation here` thrown: the action is not the contract of any implementation in `implementations`. Pass the implementation too, and select with the exact contract value it implements; an equal-looking action does not match.
292
+ - Type error at `remoteCommand(binding, action)`, or `Action "x" is not in this HTTP binding` thrown: the action was not passed to this binding's `ActionHttp.make`. Select with the exact contract value the binding received.
293
+ - Type error `Type 'HttpClient' is not assignable to type 'never'` in the pipe that runs a command over HTTP, such as through `NodeRuntime.runMain`: provide `NodeHttpClient.layerUndici` (or `FetchHttpClient.layer`). `HttpClientError` whose `reason._tag` is `InvalidUrlError`: the command's client has no `baseUrl` and prepends no URL, and routes are relative outside `Testing.layer`. Connection refused: the URL is wrong. 401 `Unauthenticated`: add credentials to the command's client, in `client`'s `transformClient` or on the client it is provided; the command adds no headers of its own.
294
+ - Another command's request goes to the API's URL, or carries its token: the client is configured around the whole program. Configure it with `remoteCommand`'s or `remote`'s `client`, or provide it on that command instead.
295
+ - `Duplicate command: <name>, claimed by action ... and action ...` thrown by `make` or `remote`: two actions have the same kebab-case name. Give one a `name` in `commands`, or aggregate them under separate `make` commands.
296
+ - `Unknown commands: <keys>` thrown by `make` or `remote`: a `commands` key names no action of the binding or the implementations. Use the action's own name, not its kebab-case command name.
297
+ - `Object literal may only specify known properties, and 'status' does not exist in type …` at `make([...apps, status], { commands: { status: ... } })` in a helper generic over implementations, `<const Apps extends ReadonlyArray<Action.AnyImplementation>>`: through that constraint, `commands` is typed for any action too, and no entry for the helper's own action compiles, `render`, `positional` or `name`. Give that action a command of its own and put both in a tree: `Command.make("x").pipe(Command.withSubcommands([ActionCli.make(apps, { name: "apps" }), ActionCli.command(status, Status, { render })]))`. `Command.withSubcommands` on `make`'s aggregate would replace its subcommands.
298
+ - Type error at `make` or `command`, an argument of type `Binding<…>` not assignable to its parameter, or at `remote` or `remoteCommand`, an implementation's: the local functions take implementations, the remote ones a binding. Call the function of the form you pass; `client` at `make` or `command`, from a literal or a variable, is refused the same way, as a command connecting nowhere. A binding or implementations chosen by a condition, `make(remotely ? Http : users, options)`, is neither: build a command from each and choose between them, `remotely ? ActionCli.remote(Http, options) : ActionCli.make(users, options)`.
299
+ - `Action "x" is implemented twice here` thrown by `command` or `make`: more than one implementation passed implements the selected action. Pass one. Other actions' names are not checked.
300
+ - Handler cannot find a service: provide its Layer on the command, `Command.provide(layer)`. The library supplies no service.
301
+ - `--help` or a mistyped flag connects to a database, or fails reading config: services are provided around the run, which builds them before parsing. Provide them on the command.
302
+ - A command requires a request-identity tag no handler yields: the action is protected, and its caller is owed whatever the handler reads. Provide the identity on the command, `Command.provideSync(CurrentActor, actor)`; do not make the contract public just to satisfy the requirement.
303
+ - Type error at `Effect.catchTag("UserNotFound", ...)` after `Command.run`: the command fails with `UserError`, whose reason is the action's failure. Use `Effect.catchReason("UserError", "UserNotFound", ...)`. `Command.run` has printed it by then: a recovery that prints nothing, or text of its own, provides a `formatError` through `CliOutput.layer`, or runs with `renderErrors: false`.
304
+ - A report with a stack on stdout, `ERROR (#1): ...`, instead of JSON on stderr: a defect, or a failure of a layer the host provides, which `runMain` reports. Fix the defect; to keep such reports and those layers' logs off stdout, apply `logToStderr` last, before `runMain`.
305
+ - Type error at `Effect.catchReason("UserError", "<Tag>", ...)`, its tag typed `never`: a failure of the command has no `_tag`, such as a builder's plain `Error`. Fail with a tagged error, such as a `Schema.TaggedError` or `Data.TaggedError`.
306
+ - `Listed in actions, but no implementation holds it: <names>`, thrown by `make`, or `but the binding does not hold it`, by `remote`: those actions are not among the implementations' or the binding's, by identity. `(another contract)` marks one whose name they hold, as a second copy of the contracts module makes. List the contract they implement or bind.
307
+ - The same report for a declared error, such as `UserNotFound`, from a command built with `Command.make`: the command calls `Action.client` or `ActionHttp.client` itself, and its failure is the action's own. Map it to `CliError.UserError`, or derive the command with `command` where its options express the syntax.