@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.
- package/README.md +102 -41
- package/dist/Action.d.mts +31 -117
- package/dist/Action.mjs +14 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +23 -29
- package/dist/ActionMcp.mjs +56 -114
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +28 -9
- package/dist/ActionToolkit.mjs +31 -4
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +30 -23
- package/dist/Testing.mjs +27 -44
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +18 -0
- package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +211 -426
- package/docs/ActionCli-failures.md +104 -0
- package/docs/ActionCli.md +208 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +205 -282
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +222 -271
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +179 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +103 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +143 -392
- package/docs/CONTEXT.md +40 -24
- package/docs/README.md +75 -35
- package/docs/Testing-failures.md +21 -0
- package/docs/Testing.md +193 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +37 -0
- package/docs/setup.md +90 -92
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +318 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +126 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +111 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +42 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +77 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +141 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +121 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +31 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +111 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +137 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +119 -0
- package/examples/notes-service/src/notes/handlers.ts +39 -0
- package/examples/notes-service/src/notes/store.ts +84 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +288 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +27 -9
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -14
- package/dist/internal/media.d.mts +0 -8
- package/dist/internal/media.mjs +0 -26
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- 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. `
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`remoteCommand`
|
|
7
|
-
|
|
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
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
| `command`, `remoteCommand`: `
|
|
25
|
-
| `command`, `remoteCommand`: `
|
|
26
|
-
| `command`, `remoteCommand`: `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `make`, `remote`: `
|
|
30
|
-
| `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
155
|
-
Command.
|
|
156
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
-
|
|
266
|
-
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
-
|
|
276
|
-
|
|
277
|
-
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
- `
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
-
|
|
308
|
-
|
|
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.
|