@gjermundgaraba/effect-actions 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -47
- package/dist/Action.d.mts +96 -49
- package/dist/Action.mjs +12 -16
- package/dist/ActionCli.d.mts +82 -44
- package/dist/ActionCli.mjs +54 -35
- package/dist/ActionHttp.d.mts +84 -45
- package/dist/ActionHttp.mjs +3 -4
- package/dist/ActionMcp.d.mts +54 -29
- package/dist/ActionMcp.mjs +33 -15
- package/dist/ActionRpc.d.mts +48 -37
- package/dist/ActionRpc.mjs +3 -4
- package/dist/ActionToolkit.d.mts +32 -15
- package/dist/ActionToolkit.mjs +11 -9
- package/dist/Authentication.d.mts +92 -42
- package/dist/Authentication.mjs +38 -37
- package/dist/Testing.d.mts +23 -27
- package/dist/Testing.mjs +1 -2
- package/dist/authentication/provider.d.mts +1 -6
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -3
- package/dist/cli/commands.d.mts +34 -13
- package/dist/cli/commands.mjs +48 -24
- package/dist/contract/ast.mjs +1 -2
- package/dist/contract/call.d.mts +1 -3
- package/dist/contract/call.mjs +1 -2
- package/dist/contract/caller.d.mts +1 -3
- package/dist/contract/caller.mjs +2 -3
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +28 -16
- package/dist/contract/errors.d.mts +42 -19
- package/dist/contract/errors.mjs +43 -19
- package/dist/contract/hints.mjs +4 -4
- package/dist/contract/implementation.d.mts +7 -14
- package/dist/contract/implementation.mjs +31 -19
- package/dist/contract/media.d.mts +7 -5
- package/dist/contract/media.mjs +7 -4
- package/dist/contract/names.mjs +15 -14
- package/dist/contract/status.mjs +4 -12
- package/dist/http/client.d.mts +15 -6
- package/dist/http/client.mjs +2 -3
- package/dist/http/schema-errors.mjs +1 -2
- package/dist/mcp/client.d.mts +21 -10
- package/dist/mcp/client.mjs +8 -11
- package/dist/mcp/protocol.mjs +1 -2
- package/dist/stdio/console.mjs +1 -2
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +1 -2
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +171 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -38
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +8 -4
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +12 -16
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- 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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
## Run actions as local commands
|
|
47
13
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
[
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
82
|
-
|
|
83
|
-
([ActionHttp.md](ActionHttp.md#call-the-api-from-effect))
|
|
84
|
-
|
|
85
|
-
[notes-service/src/
|
|
86
|
-
|
|
87
|
-
[kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
- `
|
|
12
|
-
|
|
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
|
-
- `
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
- `
|
|
81
|
-
|
|
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.
|