@gjermundgaraba/effect-actions 0.11.0 → 0.13.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 (215) hide show
  1. package/README.md +102 -41
  2. package/dist/Action.d.mts +31 -117
  3. package/dist/Action.mjs +14 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +23 -29
  9. package/dist/ActionMcp.mjs +56 -114
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +28 -9
  13. package/dist/ActionToolkit.mjs +31 -4
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +30 -23
  17. package/dist/Testing.mjs +27 -44
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +18 -0
  38. package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +211 -426
  46. package/docs/ActionCli-failures.md +104 -0
  47. package/docs/ActionCli.md +208 -293
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +205 -282
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +222 -271
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +179 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +103 -119
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +143 -392
  59. package/docs/CONTEXT.md +40 -24
  60. package/docs/README.md +75 -35
  61. package/docs/Testing-failures.md +21 -0
  62. package/docs/Testing.md +193 -151
  63. package/docs/guarantees.md +35 -96
  64. package/docs/setup-failures.md +37 -0
  65. package/docs/setup.md +90 -92
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +318 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +126 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +111 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +42 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +77 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +141 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +121 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +31 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +111 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +137 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +119 -0
  183. package/examples/notes-service/src/notes/handlers.ts +39 -0
  184. package/examples/notes-service/src/notes/store.ts +84 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +288 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +27 -9
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -14
  211. package/dist/internal/media.d.mts +0 -8
  212. package/dist/internal/media.mjs +0 -26
  213. package/dist/internal/refusal.mjs +0 -67
  214. package/dist/internal/schema-errors.mjs +0 -20
  215. package/dist/internal/tools.mjs +0 -44
package/docs/ActionCli.md CHANGED
@@ -1,308 +1,223 @@
1
1
  # ActionCli
2
2
 
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.
3
+ Native Effect CLI commands for actions, with flags derived from each action's input. `make` and
4
+ `command` take implementations and run the handler in the command's own process, behind the same
5
+ `authorize`: for a local tool, or an operator console that has the services itself. `remote` and
6
+ `remoteCommand` take an HTTP binding and call a server, running nothing locally: for users of a
7
+ service you already serve. `make` and `remote` derive a command for every action of the
8
+ implementations or the binding, so a new action is a new command: list `actions` to leave some
9
+ out.
10
+
11
+ Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
12
+ When something fails, search [ActionCli-failures.md](ActionCli-failures.md). Reading order:
13
+ [README.md](README.md).
8
14
 
9
15
  ## API
10
16
 
11
17
  Import `@gjermundgaraba/effect-actions/ActionCli`.
12
18
 
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. |
20
-
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. |
31
-
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.
91
-
92
- ## Canonical
93
-
94
- ```ts example=cli.ts
95
- import { Effect } from "effect";
96
- import { Command } from "effect/cli";
97
- import { NodeRuntime, NodeServices } from "@effect/platform-node";
98
- import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
99
- import { actors, CurrentActor } from "./authorization.js";
100
- import { userActions } from "./handlers.js";
101
- import { Users } from "./users.js";
102
-
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" },
138
- });
139
-
140
- Command.run(command, { version: "0.1.0" }).pipe(
141
- Effect.provide(NodeHttpClient.layerUndici),
142
- Effect.provide(NodeServices.layer),
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:
19
+ | API | Purpose |
20
+ | -------------------------------------------- | ------------------------------------------------------------------------ |
21
+ | `make(implementations, options)` | Every implemented action as a subcommand of one command, run in process. |
22
+ | `command(implementations, action, options?)` | One action, selected by its contract, as a native Effect `Command`. |
23
+ | `remote(binding, options)` | Every action of an HTTP binding as a subcommand, called over HTTP. |
24
+ | `remoteCommand(binding, action, options?)` | One action of an HTTP binding, called over HTTP. |
25
+ | `toUserError(binding)` | Piped onto a command of your own: it fails as a derived command does. |
26
+ | `logToStderr` | Applied last before `runMain`: stdout carries only results. |
27
+
28
+ | Option | Meaning |
29
+ | ---------------------------------------- | ------------------------------------------------------------------------------- |
30
+ | `command`, `remoteCommand`: `name` | The command's name; default: the action's in kebab case. |
31
+ | `command`, `remoteCommand`: `render` | Decoded success to text for a person; adds `--json`. |
32
+ | `command`, `remoteCommand`: `positional` | Input fields taken as positional arguments instead of flags, in this order. |
33
+ | `command`, `remoteCommand`: `aliases` | A short flag per field: `{ limit: "n" }` takes `-n 5` beside `--limit 5`. |
34
+ | `command`, `remoteCommand`: `stdin` | A required field read from stdin instead of a flag: `{ stdin: "value" }`. |
35
+ | `make`, `remote`: `name` | The aggregate's name, required. |
36
+ | `make`, `remote`: `actions` | The actions that are subcommands; default: every one. |
37
+ | `make`, `remote`: `commands` | Each subcommand's `command` options, by action name: `{ getNote: { render } }`. |
38
+ | `make`, `remote`: `subcommands` | Commands of your own beside the derived ones: `[exportNotes]`. |
39
+ | `remoteCommand`, `remote`: `client` | The `ActionHttp.client` options, `baseUrl` and `transformClient`. |
40
+
41
+ Exported types: `Options<A>`, `RemoteOptions<A>`, `CommandOptions<A>`, `RemoteCommandOptions<A>`,
42
+ and `UserError<E>`, a type only: what a command fails with when its action fails with `E`.
43
+
44
+ ## Run actions as local commands
45
+
46
+ Pass implementations to `make`, and provide the services and the caller on the command:
152
47
 
153
48
  ```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
- ),
49
+ const assistant = ActionCli.make([tasks, identity], { name: "assistant" }).pipe(
50
+ Command.provide(Layer.mergeAll(TaskStore.layerFile, Permissions.layerMemory)),
51
+ Command.provideSync(CurrentCaller, operator),
167
52
  );
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
53
  ```
172
54
 
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" };
55
+ Run it with Effect's own `Command.run`:
56
+ [assistant/src/commands.ts](../examples/assistant/src/commands.ts) and
57
+ [assistant/src/cli.ts](../examples/assistant/src/cli.ts) are the whole program, and
58
+ [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts) an operator console.
59
+
60
+ - **Provide services on the command, not around the run.** `Command.provide(layer)` builds the
61
+ layer when an action runs. Provided around `Command.run`, it is built before parsing, so
62
+ `--help` and a mistyped flag open the database. The aggregate run with no subcommand still
63
+ builds what it is provided before showing its help.
64
+ - **Each run builds its implementation.** A command runs its action's builder, and an authorizer
65
+ an Effect builds, then releases them after the call, even where the host already built them.
66
+ Implementations passed alongside are not built.
67
+ - **For one action, or a tree of your own**, use `command(implementations, action, options?)`
68
+ under native `Command.withSubcommands`. For a command of your own beside every derived one:
69
+ [Add a command of your own](#add-a-command-of-your-own).
70
+ - **Test commands without a process** with `Command.runWith`, `TestConsole` and
71
+ `Stdio.layerTest`. Provide the services around the test so its commands share one store:
72
+ [assistant/test/commands.test.ts](../examples/assistant/test/commands.test.ts).
73
+ `TestConsole.logLines` holds every line of the test, each `unknown`, not of one run: take each
74
+ run's own and compare or decode them, as
75
+ [notes-service/test/commands.test.ts](../examples/notes-service/test/commands.test.ts) does.
76
+ A run also fails with Effect CLI's own errors, such as a flag that does not parse: only a
77
+ `UserError` has a `userMessage`, so match it (`Effect.catchTag("UserError", …)`) before reading one.
78
+
79
+ ## Call a server from commands
80
+
81
+ Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler; each
82
+ command calls its action through the binding's `ActionHttp.client` method
83
+ ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)) and fails as that method does. The host
84
+ provides `HttpClient` (`NodeHttpClient.layerUndici`) and says where the server is:
85
+ [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts) and
86
+ [notes-service/src/cli.ts](../examples/notes-service/src/cli.ts).
87
+ [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header and uses
88
+ every command option.
89
+
90
+ - **Choose where the connection comes from.** A fixed URL or token goes in the `client` option:
91
+ `client: { baseUrl, transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token)) }`.
92
+ One read from `Config` configures the `HttpClient` provided on the command, with
93
+ `Command.provideEffect(HttpClient.HttpClient, …)` as `notes-service/src/cli.ts` does, so
94
+ `--help` needs neither. A command of your own in `subcommands` calls through the same client.
95
+ - **Configure the client on the command, never around the program.** Around the program it
96
+ rewrites every request the program makes, an absolute URL too, and sends each one the token.
97
+ - **A token every command needs is required, its failure mapped to a `CliError.UserError`**, or a
98
+ missing one prints a raw `ConfigError` log line: `connection` in
99
+ [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts). Where a public
100
+ command must answer signed out, read it with `Config.option` and add the header when it is
101
+ there: [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts).
102
+ - **Test a remote command the same way** as a local one, with `Testing.layer(routes)` provided
103
+ as its `HttpClient` ([Testing.md](Testing.md#test-routes-in-memory)). Export the command from
104
+ a module apart from the entrypoint that runs it
105
+ ([notes-service/test/commands.test.ts](../examples/notes-service/test/commands.test.ts)).
106
+
107
+ ## Shape flags, input and output
108
+
109
+ ### Flags from the input
110
+
111
+ A struct or class input gets one flag per top-level field, named after the field in kebab case,
112
+ with the field's description as its help, annotated before or after its checks. The field's
113
+ encoded JSON value decides the kind of flag:
114
+
115
+ | Encoded field | Flag, for a field `tags`, `read`, … |
116
+ | ---------------------------------------- | --------------------------------------------------------------- |
117
+ | string or template literal | `--name <string>` |
118
+ | boolean | `--read`, a switch: `--no-read` or `--read=false` sends `false` |
119
+ | union of string literals, or string enum | `--kind <choice>`, one of the values |
120
+ | array of strings, numbers or choices | `--tags <value>`, repeated: one element per occurrence |
121
+ | anything else, numbers included | `--owner <value>`: JSON the field accepts, or else the text |
122
+
123
+ - A switch left out is `false` for a required field, and leaves an optional one out.
124
+ - `--help` adds, after the field's description, `Repeat for each value.` on a repeated flag,
125
+ `--no-read sends false.` on an optional switch, `Left out, it sends false.` on a required
126
+ one, and `A JSON array.` or `A JSON object.` on a
127
+ value that is one, an array of objects with its fields: `A JSON array of {id, approved?}.`
128
+ - A flag takes the encoded value, which the action's schema then decodes: `"21"` for
129
+ `Schema.FiniteFromString`.
130
+ - A value flag reads its text as JSON when the field accepts that kind of JSON value
131
+ (`--count 2`, `--owner '{"id":"x"}'`, `--points '[{"x":1}]'`), and keeps it as text otherwise
132
+ (`--limit auto`).
133
+ - A repeated flag given no occurrence sends `[]` for a required `Schema.Array` field, and leaves
134
+ an optional field out, where `--tags '[]'` alone sends an empty list.
135
+ - An input that is not a struct of named fields (a union, a record, a scalar) gets one
136
+ `--input <value>` flag carrying the whole encoded input. Left off, the input is `{}`.
137
+
138
+ ### Arguments, short flags and stdin
139
+
140
+ In an aggregate, a command's options go under `commands`, keyed by the action's name
141
+ (`completeTask`), not the command's:
142
+ [assistant/src/commands.ts](../examples/assistant/src/commands.ts).
143
+
144
+ - `positional`: list every required field before any optional one, and an array field last.
145
+ - `stdin` suits a secret or a document. The value is all of stdin until it closes, without the
146
+ line endings that end it. A terminal is never read: pipe the value in.
147
+
148
+ ### Add a command of your own
149
+
150
+ Work every surface should offer, a bulk import for one, is an action first: a command of your
151
+ own then only reads the file and calls it, as `notes import <file>` reads `Argument.FileText`
152
+ and calls `importNotes`. For a syntax the options cannot express, such as a flag named apart
153
+ from its field, build the command with native `Command.make`, `Flag` and `Argument`, and pass it
154
+ in `subcommands`: `notes export <file>` takes `Argument.String("file")`
155
+ ([notes-service/src/commands.ts](../examples/notes-service/src/commands.ts)):
245
156
 
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),
250
- );
157
+ ```ts
158
+ const notes = ActionCli.remote(Http, { name: "notes", subcommands: [exportNotes] });
251
159
  ```
252
160
 
253
- ## Rules
254
-
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
- - A media field, `Action.Image`, is printed as JSON, by default and with `--json`: `{ "data": "<base64>", "mimeType": "image/png" }`, as HTTP sends it. A command's `render` receives the decoded success, the image's `data` a `Uint8Array`, and prints the text it returns instead, such as the image's size; it returns text, never bytes.
265
- - `--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.
266
- - 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.
267
- - 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.
268
- - 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.
269
- - 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.
270
- - 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.
271
- - 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.
272
- - 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.
273
- - `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)).
274
- - 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`.
275
- - 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.
276
- - 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.
277
- - 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.
278
-
279
- ## Failure modes
280
-
281
- - `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.
282
- - `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.
283
- - `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.
284
- - `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.
285
- - `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.
286
- - `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.
287
- - 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).
288
- - 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).
289
- - 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.
290
- - `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`.
291
- - A field named like a global flag (`help`, `version`, `log-level`) is not a clash: its flag shadows the global one on that command.
292
- - 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.
293
- - 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.
294
- - 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.
295
- - 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.
296
- - `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.
297
- - `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.
298
- - `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.
299
- - 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)`.
300
- - `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.
301
- - Handler cannot find a service: provide its Layer on the command, `Command.provide(layer)`. The library supplies no service.
302
- - `--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.
303
- - 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.
304
- - 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`.
305
- - 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`.
306
- - 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`.
307
- - `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.
308
- - 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.
161
+ - **Never pipe `Command.withSubcommands` onto `make` or `remote`.** It replaces the derived
162
+ commands, which then answer with the usage text.
163
+ - **Call the actions through a client**, in the command's own handler: `ActionHttp.client`, which
164
+ needs no scope and uses the `HttpClient` provided on the aggregate, or `Action.client`, acquired
165
+ inside `Effect.scoped` ([Action-in-process.md](Action-in-process.md#call-actions-in-process)).
166
+ `authorize` still runs. The `client` option reaches the derived commands alone.
167
+ - **Its output is what it prints**, with `Console.log`: the output rules below are a derived
168
+ command's. **Pipe its whole Effect through `ActionCli.toUserError(Http)`**, the binding its
169
+ client is made from, HTTP or RPC, or `toUserError([GetNote])` over `Action.client`, to fail as a derived command does: a declared
170
+ error as its JSON on stderr, a transport failure named with its causes. A message of your own is `new CliError.UserError({ cause, userMessage })`, which
171
+ passes through. To add context to a failure, such as how far a batch got, print it inside the
172
+ pipe, `Effect.tapError(() => Console.error(…))`: wrapping the `UserError` in another puts its
173
+ JSON mid-sentence and hides the action's failure from `catchReason`. A failure left unmapped, one call's included, is reported as a crash,
174
+ `ERROR (#n)` with a stack trace.
175
+ - **Write the list in the call**, or hold the options with `satisfies ActionCli.Options<…>`. The
176
+ aggregate's type then carries its commands' failures and services. A list in options
177
+ annotated `: ActionCli.Options<…>` adds none: the program compiles, then a command's service is not found.
178
+
179
+ ### Output and exit code
180
+
181
+ - **A command writes only its result to stdout**: the success as JSON, indented by two spaces,
182
+ or the text `render` returns.
183
+ - **Everything else a command runs writes to stderr**: its Effect logs and Effect `Console`
184
+ output. The global `console.log` and other direct writes bypass Effect and still reach stdout.
185
+ - **A failure of the action is printed once on stderr, under Effect CLI's `ERROR` heading, as
186
+ the JSON HTTP sends for it** ([guarantees.md](guarantees.md#wire-behavior)), on the last line:
187
+ `{"_tag":"TaskNotFound","id":"9"}`. The process exits 1, or with the failure's
188
+ `Runtime.errorExitCode`. A failure no schema encodes, a builder's or the transport's, prints
189
+ as its tag or name and its message, never its other fields.
190
+ - **Apply `logToStderr` last, before `runMain`, when scripts read stdout.** What happens outside
191
+ a command is `runMain`'s to report, on stdout: a defect, such as a handler's bug or a success
192
+ that does not encode, and a failure or a log of a layer the host provides. `logToStderr` moves
193
+ those reports and the default logger to stderr, keeping the exit code, but not those layers'
194
+ `Console` output or a logger writing through `Console.log`, such as `Logger.consoleJson`: log
195
+ JSON with `Logger.withConsoleError(Logger.formatJson)`.
196
+
197
+ ### Handle a failure after `Command.run`
198
+
199
+ A command fails with `UserError<E>`, Effect CLI's `UserError` whose `cause`, and `reason`, is the
200
+ action's failure. Match it by its tag:
201
+ `Effect.catchReason("UserError", "TaskNotFound", (missing) => ...)`. `Command.run` has printed it
202
+ by then, so a recovery decides what follows, such as the exit code, not the text. For text of
203
+ your own, provide Effect's formatter,
204
+ `CliOutput.layer(Object.assign({}, CliOutput.defaultFormatter(), { formatError }))`, or pass
205
+ `renderErrors: false` to `Command.run` and print every failure yourself, the parser's too.
206
+
207
+ ## Name the caller
208
+
209
+ A local command of a protected action requires the identity service its contract declares
210
+ ([Action.md](Action.md#say-who-may-call-an-action)), whether or not a handler reads it. No
211
+ credential arrives at a terminal, so the host names the caller, on the command:
212
+ `Command.provideSync(CurrentCaller, operator)`, or `Command.provideEffect(CurrentCaller, load)`
213
+ to read it when an action runs. See [assistant/src/cli.ts](../examples/assistant/src/cli.ts).
214
+
215
+ - **A local command is no bypass.** The implementation's `authorize` runs as on every surface
216
+ ([Action.md](Action.md#authorize-calls)).
217
+ - **A trusted operator is a type, not a flag.** Give the identity a variant only a host builds,
218
+ kept out of every verifier's return type, and admit it in the shared `authorize`:
219
+ [assistant/src/identity/Identity.ts](../examples/assistant/src/identity/Identity.ts) with
220
+ [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts), or
221
+ [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) with
222
+ [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts).
223
+ - **A remote command names no caller.** The server verifies the credentials its client sends.