@gjermundgaraba/effect-actions 0.18.2 → 0.19.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 (144) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -47
  3. package/dist/Action.mjs +11 -14
  4. package/dist/ActionCli.d.mts +81 -42
  5. package/dist/ActionCli.mjs +52 -33
  6. package/dist/ActionHttp.d.mts +84 -43
  7. package/dist/ActionHttp.mjs +2 -2
  8. package/dist/ActionMcp.d.mts +54 -27
  9. package/dist/ActionMcp.mjs +5 -5
  10. package/dist/ActionRpc.d.mts +48 -35
  11. package/dist/ActionRpc.mjs +1 -1
  12. package/dist/ActionToolkit.d.mts +32 -13
  13. package/dist/ActionToolkit.mjs +10 -7
  14. package/dist/Authentication.d.mts +92 -40
  15. package/dist/Authentication.mjs +37 -35
  16. package/dist/Testing.d.mts +22 -24
  17. package/dist/authentication/provider.d.mts +1 -4
  18. package/dist/authentication/provider.mjs +1 -1
  19. package/dist/authentication/refusal.mjs +2 -1
  20. package/dist/cli/commands.d.mts +34 -11
  21. package/dist/cli/commands.mjs +47 -22
  22. package/dist/contract/caller.mjs +1 -1
  23. package/dist/contract/declared.mjs +27 -14
  24. package/dist/contract/errors.d.mts +42 -17
  25. package/dist/contract/errors.mjs +42 -17
  26. package/dist/contract/hints.mjs +3 -2
  27. package/dist/contract/implementation.d.mts +5 -8
  28. package/dist/contract/implementation.mjs +29 -17
  29. package/dist/contract/media.d.mts +6 -2
  30. package/dist/contract/media.mjs +6 -2
  31. package/dist/contract/names.mjs +14 -12
  32. package/dist/contract/status.mjs +3 -10
  33. package/dist/http/client.d.mts +15 -4
  34. package/dist/http/client.mjs +1 -1
  35. package/dist/mcp/client.d.mts +21 -8
  36. package/dist/mcp/client.mjs +6 -8
  37. package/docs/Action-failures.md +6 -15
  38. package/docs/Action.md +159 -191
  39. package/docs/ActionCli-failures.md +12 -92
  40. package/docs/ActionCli.md +102 -156
  41. package/docs/ActionHttp-failures.md +14 -76
  42. package/docs/ActionHttp.md +150 -176
  43. package/docs/ActionMcp-failures.md +2 -22
  44. package/docs/ActionMcp.md +79 -170
  45. package/docs/ActionRpc-failures.md +3 -12
  46. package/docs/ActionRpc.md +183 -120
  47. package/docs/ActionToolkit-failures.md +3 -10
  48. package/docs/ActionToolkit.md +112 -109
  49. package/docs/Authentication-advanced.md +167 -108
  50. package/docs/Authentication-failures.md +11 -91
  51. package/docs/Authentication.md +114 -135
  52. package/docs/README.md +45 -30
  53. package/docs/Testing-failures.md +2 -6
  54. package/docs/Testing.md +126 -159
  55. package/docs/setup-failures.md +8 -32
  56. package/docs/setup.md +42 -37
  57. package/examples/README.md +6 -10
  58. package/examples/assistant/README.md +20 -38
  59. package/examples/assistant/src/agent.ts +2 -3
  60. package/examples/assistant/src/cli.ts +4 -9
  61. package/examples/assistant/src/commands.ts +6 -4
  62. package/examples/assistant/src/identity/authorize.ts +3 -3
  63. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  64. package/examples/assistant/src/tasks/handlers.ts +3 -3
  65. package/examples/assistant/src/toolkit.ts +6 -8
  66. package/examples/assistant/test/commands.test.ts +16 -11
  67. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  68. package/examples/assistant/test/toolkit.test.ts +0 -1
  69. package/examples/kitchen-sink/README.md +30 -52
  70. package/examples/kitchen-sink/src/Actions.ts +2 -3
  71. package/examples/kitchen-sink/src/Api.ts +3 -4
  72. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  73. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  74. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  75. package/examples/kitchen-sink/src/agent.ts +1 -1
  76. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  77. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  78. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  79. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  80. package/examples/kitchen-sink/src/browser.ts +3 -4
  81. package/examples/kitchen-sink/src/cli.ts +6 -7
  82. package/examples/kitchen-sink/src/config.ts +2 -0
  83. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  84. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  85. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  86. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  87. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  88. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  89. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  90. package/examples/kitchen-sink/src/ops.ts +3 -6
  91. package/examples/kitchen-sink/src/request-policy.ts +3 -3
  92. package/examples/kitchen-sink/src/routes.ts +4 -6
  93. package/examples/kitchen-sink/src/seed.ts +0 -1
  94. package/examples/kitchen-sink/src/server.ts +11 -12
  95. package/examples/kitchen-sink/src/stdio.ts +3 -2
  96. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  97. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  98. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  99. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  100. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  101. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  102. package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
  103. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  104. package/examples/live-rpc/README.md +8 -49
  105. package/examples/live-rpc/src/browser.ts +8 -41
  106. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  107. package/examples/live-rpc/src/chat/room.ts +1 -1
  108. package/examples/live-rpc/src/client.ts +2 -3
  109. package/examples/live-rpc/src/page.ts +34 -0
  110. package/examples/live-rpc/src/routes.ts +6 -7
  111. package/examples/live-rpc/src/server.ts +6 -4
  112. package/examples/live-rpc/test/browser.test.ts +6 -5
  113. package/examples/live-rpc/test/routes.test.ts +16 -15
  114. package/examples/live-rpc/test/server.test.ts +1 -1
  115. package/examples/mcp-tools/README.md +22 -45
  116. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  117. package/examples/mcp-tools/src/routes.ts +0 -7
  118. package/examples/mcp-tools/src/server.ts +1 -1
  119. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  120. package/examples/mcp-tools/test/routes.test.ts +2 -3
  121. package/examples/notes-service/README.md +9 -43
  122. package/examples/notes-service/src/cli.ts +0 -3
  123. package/examples/notes-service/src/commands.ts +7 -17
  124. package/examples/notes-service/src/identity/authorize.ts +2 -5
  125. package/examples/notes-service/src/identity/verifier.ts +3 -4
  126. package/examples/notes-service/src/notes/Notes.ts +2 -6
  127. package/examples/notes-service/src/notes/store.ts +5 -1
  128. package/examples/notes-service/src/routes.ts +6 -8
  129. package/examples/notes-service/src/server.ts +4 -3
  130. package/examples/notes-service/src/stdio.ts +4 -3
  131. package/examples/notes-service/test/commands.test.ts +1 -1
  132. package/examples/notes-service/test/notes/as.ts +6 -0
  133. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  134. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  135. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  136. package/examples/notes-service/test/routes.test.ts +2 -2
  137. package/examples/quickstart/README.md +1 -1
  138. package/examples/quickstart/src/client.ts +1 -0
  139. package/examples/quickstart/src/server.ts +2 -4
  140. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  141. package/package.json +1 -1
  142. package/docs/Action-in-process.md +0 -93
  143. package/docs/CONTEXT.md +0 -46
  144. package/docs/guarantees.md +0 -44
package/docs/ActionCli.md CHANGED
@@ -4,102 +4,74 @@ Native Effect CLI commands for actions, with flags derived from each action's in
4
4
  `command` take implementations and run the handler in the command's own process, behind the same
5
5
  `authorize`: for a local tool, or an operator console that has the services itself. `remote` and
6
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).
14
-
15
- ## API
16
-
17
- Import `@gjermundgaraba/effect-actions/ActionCli`.
18
-
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 client its commands call: `ActionHttp.client(Http, { baseUrl })`. |
40
-
41
- Exported types: `Options<A>` of `make` and `remote`, `CommandOptions<A>` of `command` and
42
- `remoteCommand`, and `UserError<E>`, a type only: what a command fails with when its action fails with `E`.
7
+ service you already serve. `make` and `remote` derive one command per action; which actions a
8
+ surface serves is on `Action.make`.
43
9
 
44
- ## Run actions as local commands
10
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionCli-failures.md](ActionCli-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
45
11
 
46
- Pass implementations to `make`, and provide the services and the caller on the command:
12
+ ## Run actions as local commands
47
13
 
48
- ```ts
49
- const assistant = ActionCli.make([tasks, identity], { name: "assistant" }).pipe(
14
+ Pass implementations to `make`; the host provides the services and the caller on the command,
15
+ and runs it with Effect's own `Command.run`:
16
+
17
+ ```ts example=assistant/src/cli.ts
18
+ import { userInfo } from "node:os";
19
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
20
+ import { Effect, Layer } from "effect";
21
+ import { Command } from "effect/cli";
22
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
23
+ import { commands } from "./commands.js";
24
+ import { CurrentCaller, type Operator } from "./identity/Identity.js";
25
+ import { Permissions } from "./identity/permissions.js";
26
+ import { tasksFile } from "./platform/tasks-file.js";
27
+
28
+ // Entrypoint: `node --import tsx examples/assistant/src/cli.ts list-tasks`.
29
+ // The operator's own tool. No credential arrives at a terminal, so this host names the caller
30
+ // itself: whoever runs the command, as the trusted operator. Being local earns nothing more:
31
+ // `authorize` runs here as for a model's tool call, and admits the operator by its kind.
32
+ const operator: Operator = { kind: "operator", id: userInfo().username };
33
+
34
+ const assistant = commands.pipe(
50
35
  Command.provide(Layer.mergeAll(tasksFile, Permissions.layerMemory)),
51
36
  Command.provideSync(CurrentCaller, operator),
52
37
  );
38
+
39
+ // A command's defect, such as a tasks file that does not decode, `runMain` reports on stdout.
40
+ // `logToStderr`, applied last, moves that to stderr, so `assistant list-tasks --json | jq` reads
41
+ // results alone.
42
+ Command.run(assistant, { version: "1.0.0" }).pipe(
43
+ Effect.provide(NodeServices.layer),
44
+ ActionCli.logToStderr,
45
+ NodeRuntime.runMain,
46
+ );
53
47
  ```
54
48
 
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).
49
+ The aggregate is [assistant/src/commands.ts](../examples/assistant/src/commands.ts), and
50
+ [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts) an operator console. For one
51
+ action, or a tree of your own under native `Command.withSubcommands`, use
52
+ `command(implementations, action, options?)`. Where services go, what each run builds and what a
53
+ command prints on which stream are on `ActionCli.command`; what `runMain` reports, a command's
54
+ defect included, on `ActionCli.logToStderr`; matching a failure after `Command.run`, on
55
+ `ActionCli.UserError`.
56
+
70
57
  - **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.
58
+ `Stdio.layerTest`: [assistant/test/commands.test.ts](../examples/assistant/test/commands.test.ts).
78
59
 
79
60
  ## Call a server from commands
80
61
 
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`) in
85
- [notes-service/src/cli.ts](../examples/notes-service/src/cli.ts); the client says where the
86
- server is in [notes-service/src/commands.ts](../examples/notes-service/src/commands.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
- - **The connection is the `client` option**, an Effect of the binding's client run each time a
91
- command runs: `client: ActionHttp.client(Http, { baseUrl, transformClient })`. One reading
92
- `Config` is an `Effect.gen` returning `yield* ActionHttp.client(…)`, so `--help` reads nothing.
93
- Its failures and its methods' are the command's, printed as `Tag: message`, a
94
- `CliError.UserError` as its `userMessage`; what they require, the host provides. Credentials go in `transformClient`
95
- ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)).
96
- - **Never transform the `HttpClient` around the program**: it rewrites every request the program
97
- makes, an absolute URL too, and sends each one the token.
98
- - **A token every command needs is required, its failure mapped to a tagged error whose `message`
99
- says what to do**, or a missing one prints as `ConfigError: …`: `api` in
100
- [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts). Where a public
101
- command must answer signed out, read it with `Config.option` and add the header when it is
102
- there: [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts).
62
+ Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler. The
63
+ connection is the `client` option, an `ActionHttp.client`
64
+ ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)), its rules on `ActionCli.Calling`'s `client`,
65
+ shared by `remote` and `remoteCommand`:
66
+ [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts) requires a token,
67
+ mapping its absence to an error that says what to do, and
68
+ [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header, reads an
69
+ optional token for a public command. The host provides
70
+ `HttpClient` (`NodeHttpClient.layerUndici`) in
71
+ [notes-service/src/cli.ts](../examples/notes-service/src/cli.ts).
72
+
73
+ - **Split one binding into groups of commands**, one `remote` per group with its own `actions`;
74
+ what each group's `client` needs is on `ActionCli.Calling`'s `client`.
103
75
  - **Test a remote command the same way** as a local one, with `Testing.layer(routes)` provided
104
76
  as its `HttpClient` ([Testing.md](Testing.md#test-routes-in-memory)). Export the command from
105
77
  a module apart from the entrypoint that runs it
@@ -130,7 +102,8 @@ encoded JSON value decides the kind of flag:
130
102
  `Schema.FiniteFromString`.
131
103
  - A value flag reads its text as JSON when the field accepts that kind of JSON value
132
104
  (`--count 2`, `--owner '{"id":"x"}'`, `--points '[{"x":1}]'`), and keeps it as text otherwise
133
- (`--limit auto`).
105
+ (`--limit auto`): quote a string that would read as JSON, `--id '"123"'` for a
106
+ `String | Number` field.
134
107
  - A repeated flag given no occurrence sends `[]` for a required `Schema.Array` field, and leaves
135
108
  an optional field out, where `--tags '[]'` alone sends an empty list.
136
109
  - An input that is not a struct of named fields (a union, a record, a scalar) gets one
@@ -138,88 +111,61 @@ encoded JSON value decides the kind of flag:
138
111
 
139
112
  ### Arguments, short flags and stdin
140
113
 
141
- In an aggregate, a command's options go under `commands`, keyed by the action's name
142
- (`completeTask`), not the command's:
143
- [assistant/src/commands.ts](../examples/assistant/src/commands.ts).
144
-
145
- - `positional`: list every required field before any optional one, and an array field last.
146
- - `stdin` suits a secret or a document. The value is all of stdin until it closes, without the
147
- line endings that end it. A terminal is never read: pipe the value in.
114
+ `description`, `render`, `positional`, `aliases` and `stdin` shape one command; in an aggregate
115
+ they go under `commands`, keyed by the action's name. Each one's rules are on its own
116
+ declaration:
117
+
118
+ ```ts example=assistant/src/commands.ts
119
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
120
+ import { identity } from "./identity/handlers.js";
121
+ import { tasks } from "./tasks/handlers.js";
122
+
123
+ // The same implementations as local commands, `assistant <action>`: a command runs the handler
124
+ // in this process, behind the same `authorize`. `add-task` and `who-am-i` are as derived; the
125
+ // others each change their syntax or output.
126
+ export const commands = ActionCli.make([tasks, identity], {
127
+ name: "assistant",
128
+ commands: {
129
+ // `assistant list-tasks -a ada`: a short flag beside `--assignee`. `render` prints a line
130
+ // per task for a person, and adds `--json` for the JSON a script reads. `description` is the
131
+ // command's help, saying what a line holds; the action's stays what a model reads.
132
+ listTasks: {
133
+ description: "Print one line per task: [x] when done, then its id, title and assignee.",
134
+ aliases: { assignee: "a" },
135
+ render: ({ tasks }) =>
136
+ tasks
137
+ .map((task) => `${task.done ? "[x]" : "[ ]"} ${task.id} ${task.title} (${task.assignee})`)
138
+ .join("\n"),
139
+ },
140
+ // `assistant complete-task 2`: an argument instead of `--id 2`.
141
+ completeTask: { positional: ["id"] },
142
+ // `assistant annotate-task --id 2 < notes.md`: the notes are piped in, not a flag.
143
+ annotateTask: { stdin: "notes" },
144
+ },
145
+ });
146
+ ```
148
147
 
149
148
  ### Add a command of your own
150
149
 
151
150
  Work every surface should offer, a bulk import for one, is an action first: a command of your
152
- own then only reads the file and calls it. Its client checks the input as it encodes it, naming
153
- a bad entry by its path, so a decode of your own only repeats that check. `notes import <file>`
154
- reads `Argument.FileText` and calls `importNotes`. For a syntax the options cannot express, such
155
- as a flag named apart from its field, build the command with native `Command.make`, `Flag` and `Argument`, and pass it
156
- in `subcommands`: `notes export <file>` takes `Argument.String("file")`
157
- ([notes-service/src/commands.ts](../examples/notes-service/src/commands.ts)):
158
-
159
- ```ts
160
- const notes = ActionCli.remote(Http, { name: "notes", client: api, subcommands: [exportNotes] });
161
- ```
162
-
163
- - **Never pipe `Command.withSubcommands` onto `make` or `remote`.** It replaces the derived
164
- commands, which then answer with the usage text.
165
- - **Call the actions through a client**, in the command's own handler: `yield* api`, the Effect
166
- the aggregate's `client` is, which needs no scope, or `Action.client`, acquired inside
167
- `Effect.scoped` ([Action-in-process.md](Action-in-process.md#call-actions-in-process)).
168
- `authorize` still runs.
169
- - **Its output is what it prints**, with `Console.log`: the output rules below are a derived
170
- command's. **Pipe its whole Effect through `ActionCli.toUserError(Http)`**, the binding its
171
- client is made from, HTTP or RPC, or `toUserError([GetNote])` over `Action.client`, to fail as a derived command does: a declared
172
- 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
173
- passes through. To add context to a failure, such as how far a batch got, print it inside the
174
- pipe, `Effect.tapError(() => Console.error(…))`: wrapping the `UserError` in another puts its
175
- JSON mid-sentence and hides the action's failure from `catchReason`. A failure left unmapped, one call's included, is reported as a crash,
176
- `ERROR (#n)` with a stack trace.
177
- - **Write the list in the call**, or hold the options with `satisfies ActionCli.Options<…>`. The
178
- aggregate's type then carries its commands' failures and services. A list in options
179
- annotated `: ActionCli.Options<…>` adds none: the program compiles, then a command's service is not found.
180
-
181
- ### Output and exit code
182
-
183
- - **A command writes only its result to stdout**: the success as JSON, indented by two spaces,
184
- or the text `render` returns.
185
- - **Everything else a command runs writes to stderr**: its Effect logs and Effect `Console`
186
- output. The global `console.log` and other direct writes bypass Effect and still reach stdout.
187
- - **A failure of the action is printed once on stderr, under Effect CLI's `ERROR` heading, as
188
- the JSON HTTP sends for it** ([guarantees.md](guarantees.md#wire-behavior)), on the last line:
189
- `{"_tag":"TaskNotFound","id":"9"}`. The process exits 1, or with the failure's
190
- `Runtime.errorExitCode`. A failure no schema encodes, a builder's or the transport's, prints
191
- as its tag or name and its message, never its other fields.
192
- - **Apply `logToStderr` last, before `runMain`, when scripts read stdout.** What happens outside
193
- a command is `runMain`'s to report, on stdout: a defect, such as a handler's bug or a success
194
- that does not encode, and a failure or a log of a layer the host provides. `logToStderr` moves
195
- those reports and the default logger to stderr, keeping the exit code, but not those layers'
196
- `Console` output or a logger writing through `Console.log`, such as `Logger.consoleJson`: log
197
- JSON with `Logger.withConsoleError(Logger.formatJson)`.
198
-
199
- ### Handle a failure after `Command.run`
200
-
201
- A command fails with `UserError<E>`, Effect CLI's `UserError` whose `cause`, and `reason`, is the
202
- action's failure. Match it by its tag:
203
- `Effect.catchReason("UserError", "TaskNotFound", (missing) => ...)`. `Command.run` has printed it
204
- by then, so a recovery decides what follows, such as the exit code, not the text. For text of
205
- your own, provide Effect's formatter,
206
- `CliOutput.layer(Object.assign({}, CliOutput.defaultFormatter(), { formatError }))`, or pass
207
- `renderErrors: false` to `Command.run` and print every failure yourself, the parser's too.
151
+ own then only reads the file and calls it. For a syntax the options cannot express, such as a
152
+ flag named apart from its field, build the command with native `Command.make`, `Flag` and
153
+ `Argument`, and pass it in `subcommands`, whose rules are on `ActionCli.Options`; pipe its
154
+ Effect through `toUserError` to fail as a derived command does.
155
+ In [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts),
156
+ `notes import <file>` reads `Argument.FileText` and calls `importNotes`, and
157
+ `notes export <file>` takes `Argument.String("file")` and calls two actions.
208
158
 
209
159
  ## Name the caller
210
160
 
211
- A local command of a protected action requires the identity service its contract declares
212
- ([Action.md](Action.md#say-who-may-call-an-action)), whether or not a handler reads it. No
213
- credential arrives at a terminal, so the host names the caller, on the command:
214
- `Command.provideSync(CurrentCaller, operator)`, or `Command.provideEffect(CurrentCaller, load)`
215
- to read it when an action runs. See [assistant/src/cli.ts](../examples/assistant/src/cli.ts).
161
+ `Command.provideSync(CurrentCaller, operator)` names the caller on the command in
162
+ [assistant/src/cli.ts](../examples/assistant/src/cli.ts), or
163
+ `Command.provideEffect(CurrentCaller, load)` reads it when an action runs
164
+ ([Action.md](Action.md#say-who-may-call-an-action)).
216
165
 
217
- - **A local command is no bypass.** The implementation's `authorize` runs as on every surface
218
- ([Action.md](Action.md#authorize-calls)).
219
166
  - **A trusted operator is a type, not a flag.** Give the identity a variant only a host builds,
220
167
  kept out of every verifier's return type, and admit it in the shared `authorize`:
221
168
  [assistant/src/identity/Identity.ts](../examples/assistant/src/identity/Identity.ts) with
222
169
  [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts), or
223
170
  [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) with
224
171
  [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts).
225
- - **A remote command names no caller.** The server verifies the credentials its client sends.
@@ -5,85 +5,23 @@ The tasks are in [ActionHttp.md](ActionHttp.md).
5
5
 
6
6
  ## Binding and serving
7
7
 
8
- - A route returns 404, which a client method reports as an `HttpClientError` with `DecodeError`
9
- or `StatusCodeError`: the path lacks the prefix, no `layer` call serves the action, or the
10
- binding does not hold that very action value, as when a test declares the contract again.
11
- - `No action of these implementations is in this HTTP binding: x`,
12
- `Listed in actions, but the binding does not hold it: <names>` or
13
- `but no implementation holds it`, thrown by `layer`: the binding, the implementations and
14
- `actions` are matched by action value, never by name. `x (another contract)` is an action of a
15
- bound name that is not the bound value, as a second copy of the contracts module makes.
16
- - `Duplicate served action: <name>` thrown by `layer`, or
17
- `Method 'POST' already declared for route '<prefix>/<action>'` when the host builds: an action
18
- is served once. One `layer` call got two implementations of it, or two calls serve it: give
19
- each layer its own `actions`.
20
- - `Duplicate action: <name>` thrown by `make`: two actions share a name, or one is listed twice.
21
- Rename one, or bind it under another prefix.
22
- - `ActionHttp binding: error _tag "Forbidden" is built in, and declared on every surface` thrown by
23
- `make`: a binding error is a built-in error or reuses its tag. Drop it, or rename your own.
24
- - `Duplicate OpenAPI operationId: <name>` from `OpenApi.fromApi`, or a combined document or
25
- native client lacking one binding's actions: bindings combined into one `HttpApi` need distinct
26
- action names and distinct prefixes. Rename or re-prefix one, or document each binding alone.
27
- - `Service not found: effect-actions/Authentication/Security/…` when the host builds: a binding
28
- holding a protected action is served with the native `HttpApiBuilder`. Serve it with `layer`.
29
- - Swagger shows no Authorize button: the binding holds no protected action.
8
+ - A route returns 404, which a client method reports as an `HttpClientError` with `DecodeError` or `StatusCodeError`: the path lacks the prefix, no `ActionHttp.layer` call serves the action, or the binding does not hold that very action value, as when a test declares the contract again.
9
+ - `Method 'POST' already declared for route '<prefix>/<action>'` when the host builds: two `ActionHttp.layer` calls serve one action; give each layer its own `actions`.
10
+ - `Duplicate OpenAPI operationId: <name>` from `OpenApi.fromApi`: two bindings combined into one `HttpApi` share an action name; rename one, or document each binding alone.
11
+ - `Type 'Handlers<…>' is not assignable to type '`Endpoint not handled: ${string}`'` at `HttpApiBuilder.group(binding.api, …)`, or `Service not found: effect-actions/Authentication/Security/…` when the host builds: a binding is served with the native `HttpApiBuilder`; serve it with `ActionHttp.layer`.
12
+ - `Unsupported content-type: <type>` as a 415 body: Effect refuses a `Content-Type` other than `application/json`, after authentication and before the handler; send `application/json`.
30
13
 
31
14
  ## Types
32
15
 
33
- - `No overload matches this call` at `make`, naming
34
- `"Protected actions take options naming their authentication"`, or a type error naming
35
- `authentication`: give `{ authentication: Login }`. Do not silence it with
36
- `caller: Action.Anyone`.
37
- - `Argument of type 'Options<…> | undefined' is not assignable` at `make`: a helper forwards
38
- options that may be absent. Pass `options ?? {}`.
39
- - `Provider<…>` among the layer's requirements, or
40
- `Type 'Provider<...>' is not assignable to type 'never'` where the server is launched: provide
41
- the descriptor's provider ([Authentication-failures.md](Authentication-failures.md)).
42
- - `Authentication "<name>": the binding's descriptor is not its provider's`, a defect when `layer`
43
- builds: build the binding and its provider from one descriptor.
44
- - `Type 'X' is not assignable to type 'never'` where the server is launched, or
45
- `Request<"Requires", X>` in the layer's type: a handler, an authorizer or a layer middleware
46
- reads a request service `X` that nothing around the layer provides, or router middleware
47
- provides `X` and is not declared `HttpRouter.middleware<{ provides: X }>()`
48
- ([Serve the routes](ActionHttp.md#serve-the-routes)). Where `X` is the identity, a public
49
- handler reads it, or a layer whose middleware requires it serves public actions: make the
50
- action protected, or serve the protected actions from a layer of their own.
51
- - Type error on `middleware` naming
52
- `"Layer middleware fails only with the binding's errors and needs no client"`: add the error to
53
- the binding's `error`, given inline so its type is certain, or move the check to the handlers
54
- and declare the error on their actions.
55
-
56
- - `No overload matches this call` at `client`, naming `Options<unknown, unknown>`: options built
57
- before the call hold the generic `HttpClient.mapRequestEffect(authorize)`, whose failures the
58
- call cannot infer, or a helper types them as `Parameters<typeof ActionHttp.client>[1]`. Write
59
- the transform as `(client) => HttpClient.mapRequestEffect(client, authorize)`, and type such
60
- options as `ActionHttp.ClientOptions<E, R>`.
61
-
62
- ## Requests
63
-
64
- - A route answers without credentials: its contract is `caller: Action.Anyone`.
65
- - A protected route answers 401 to a request with no content type or invalid input:
66
- authentication runs before the body is read. Send the token.
67
- - 400 `InvalidInput` on a valid-looking request: its `message` names each field that did not
68
- decode or the action does not declare, such as a misspelling. An action without input takes
69
- only `{}`, and needs a body.
70
- - 415 `Unsupported content-type: <type>` (`none` without one): the request is not typed as JSON,
71
- such as `fetch`'s default `text/plain` for a string body. Send `Content-Type: application/json`.
72
- - Empty 500: a defect, or a handler result not matching the success schema. The cause is in the
73
- server's logs, written by its request logger, and under `Testing.layer` in the test's, as an error.
16
+ - `Argument of type 'Options<…> | undefined' is not assignable` at `ActionHttp.make`: a helper forwards options that may be absent; pass `options ?? {}`.
17
+ - `Type 'X' is not assignable to type 'never'` where the server is launched, or `Request<"Requires", X>` in the layer's type: nothing around the layer provides the request service `X`, or router middleware provides it undeclared; declare it `HttpRouter.middleware<{ provides: X }>()`; the rules are on `ActionHttp.layer`, and where `X` is the identity, on `ActionHttp.LayerOptions.middleware`.
18
+ - Type error on `ActionHttp.LayerOptions.middleware` naming `"Layer middleware fails only with the binding's errors and needs no client"`: add the error to the binding's `error`, given inline so its type is certain, or move the check to the handlers and declare the error on their actions.
19
+ - `No overload matches this call` at `ActionHttp.client`, naming `Options<unknown, unknown>`: options built before the call hold the generic `HttpClient.mapRequestEffect(authorize)`, or a helper types them as `Parameters<typeof ActionHttp.client>[1]`; write `(client) => HttpClient.mapRequestEffect(client, authorize)`, and type such options as `ActionHttp.ClientOptions<E, R>`.
74
20
 
75
21
  ## Client methods
76
22
 
77
- - `HttpClientError` whose `reason._tag` is `DecodeError`, printed `Decode error (<status> …)`,
78
- for a status such as 429: no schema declares that status. For a 500, see the empty 500 above. Declare the error on the binding when middleware answers with it, or on
79
- the action when its handler does.
80
- - `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: relative routes
81
- have no page to resolve against. Set `baseUrl`.
82
- - Type error listing `HttpClient` as an unsatisfied requirement: provide one, such as
83
- `FetchHttpClient.layer`, or use `fetchClient`.
84
- - `Expected 1 arguments` at a client method: `{}` is not a valid input for the action, so pass
85
- its input. Where the argument may be omitted, leave it out: `undefined` is a type error.
86
- - Type error passing `{ payload: ... }` to a client method: `payload` is the native client's
87
- shape. Pass the input itself.
88
- - `InvalidInput` `Expected Filters` given a plain object: the input is a class, which encodes
89
- only its instances. Pass `new Filters({ ... })`.
23
+ - `HttpClientError` whose `reason._tag` is `DecodeError`, printed `Decode error (<status> …)`, for a status such as 429: no schema declares that status; declare the error on the binding when middleware answers with it, on the action when its handler does. For a 500, the cause is in the server's logs.
24
+ - `HttpClientError` whose `reason._tag` is `InvalidUrlError` outside a browser: relative routes have no page to resolve against; set `baseUrl`.
25
+ - Type error listing `HttpClient` as an unsatisfied requirement: provide one, such as `FetchHttpClient.layer`, or use `ActionHttp.fetchClient`.
26
+ - `Expected 1 arguments` at a client method: `{}` is not a valid input for the action, so pass its input; where the argument may be omitted, leave it out, as `undefined` is a type error.
27
+ - Type error passing `{ payload: ... }` to a client method: `payload` is the native client's shape; pass the input itself.