@gjermundgaraba/effect-actions 0.9.0 → 0.11.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 (70) hide show
  1. package/README.md +56 -50
  2. package/dist/Action.d.mts +286 -79
  3. package/dist/Action.mjs +125 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +231 -80
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +84 -57
  15. package/dist/Testing.mjs +121 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +14 -0
  34. package/dist/internal/mcp.mjs +182 -0
  35. package/dist/internal/media.d.mts +8 -0
  36. package/dist/internal/media.mjs +26 -0
  37. package/dist/internal/memory.d.mts +6 -0
  38. package/dist/internal/memory.mjs +38 -0
  39. package/dist/internal/refusal.mjs +67 -0
  40. package/dist/internal/schema-errors.mjs +20 -0
  41. package/dist/internal/tools.mjs +27 -57
  42. package/docs/Action.md +403 -73
  43. package/docs/ActionCli.md +281 -51
  44. package/docs/ActionHttp.md +267 -82
  45. package/docs/ActionMcp.md +232 -102
  46. package/docs/ActionToolkit.md +99 -35
  47. package/docs/Authentication.md +369 -62
  48. package/docs/CONTEXT.md +24 -36
  49. package/docs/README.md +29 -73
  50. package/docs/Testing.md +154 -77
  51. package/docs/guarantees.md +82 -43
  52. package/docs/setup.md +95 -27
  53. package/package.json +12 -20
  54. package/dist/ActionCatalog.d.mts +0 -30
  55. package/dist/ActionCatalog.mjs +0 -39
  56. package/dist/ActionCliClient.d.mts +0 -48
  57. package/dist/ActionCliClient.mjs +0 -43
  58. package/dist/ActionGroup.d.mts +0 -44
  59. package/dist/ActionGroup.mjs +0 -35
  60. package/dist/ActionHttpClient.d.mts +0 -33
  61. package/dist/ActionHttpClient.mjs +0 -16
  62. package/dist/TestingClient.d.mts +0 -17
  63. package/dist/TestingClient.mjs +0 -22
  64. package/dist/internal/fetchClient.d.mts +0 -6
  65. package/dist/internal/fetchClient.mjs +0 -8
  66. package/dist/internal/tools.d.mts +0 -22
  67. package/docs/ActionCatalog.md +0 -54
  68. package/docs/ActionCliClient.md +0 -63
  69. package/docs/ActionGroup.md +0 -128
  70. package/docs/ActionHttpClient.md +0 -80
package/dist/Action.mjs CHANGED
@@ -1,32 +1,134 @@
1
- import { assertName } from "./internal/actions.mjs";
2
- import { Schema } from "effect";
1
+ import { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated } from "./internal/errors.mjs";
2
+ import { Anyone, assertKnown, assertName, assertOnce, assertOwnTags, errorList, projectedErrors } from "./internal/actions.mjs";
3
+ import { inputOf } from "./internal/call.mjs";
4
+ import { Implementation, builders, built, memoized, select, toList } from "./internal/implementation.mjs";
5
+ import { Image } from "./internal/media.mjs";
6
+ import { Array as Array$1, Cause, Context, Effect, Predicate, Schema } from "effect";
3
7
  //#region src/Action.ts
4
- /** An empty object schema that also produces the object root MCP requires. */
8
+ /**
9
+ * An object without fields: an action without arguments, `{}` given or not. Effect's own
10
+ * `Tool.EmptyParams`: strict, unlike `Schema.Struct({})`, which accepts any value but `null`,
11
+ * and the object root MCP requires.
12
+ */
5
13
  const NoInput = Schema.Record(Schema.String, Schema.Never);
14
+ /**
15
+ * The schema an option stands for: a schema as given, `NoInput` for no fields, or the struct
16
+ * of the fields, which may be keyed by symbols, as in a struct.
17
+ */
18
+ const codecOf = (schema) => Schema.isSchema(schema) ? schema : Reflect.ownKeys(schema).length === 0 ? NoInput : Schema.Struct(schema);
6
19
  function make(name, options) {
7
20
  assertName("action name", name);
8
- const { access } = options;
9
- if (access !== "read" && access !== "write") throw new Error(`Invalid access: ${String(access)}`);
10
- const readOnly = options.mcp === false ? false : options.mcp?.readOnly ?? access === "read";
11
- const mcp = options.mcp === false ? false : {
12
- name: options.mcp?.name ?? name,
13
- readOnly,
14
- destructive: options.mcp?.destructive ?? !readOnly,
15
- idempotent: options.mcp?.idempotent ?? false,
16
- openWorld: options.mcp?.openWorld ?? true,
17
- ...options.mcp?.text === void 0 ? {} : { text: options.mcp.text }
18
- };
19
- if (mcp !== false && mcp.name.length > 128) throw new Error(`Invalid MCP name: ${mcp.name}`);
20
- if (mcp !== false) assertName("MCP name", mcp.name);
21
- return {
21
+ const { caller, readOnly } = options;
22
+ if (caller !== Anyone && !Context.isKey(caller)) throw new Error("Missing caller: declare Action.Anyone or an identity service key");
23
+ if (caller !== Anyone && Context.isReference(caller)) throw new Error("Invalid caller: an identity is a Context.Service, not a Context.Reference");
24
+ if (readOnly !== true && readOnly !== false) throw new Error(`Invalid readOnly: ${String(readOnly)}`);
25
+ const action = {
26
+ caller,
22
27
  name,
23
28
  description: options.description,
24
- input: options.input ?? NoInput,
25
- success: options.success,
26
- errors: options.errors ?? [],
27
- access,
28
- mcp
29
+ input: codecOf(options.input ?? {}),
30
+ success: options.success === void 0 ? Schema.Void : codecOf(options.success),
31
+ error: errorList(options.error),
32
+ readOnly,
33
+ mcp: { ...options.mcp }
34
+ };
35
+ assertOwnTags(`Action "${action.name}"`, action.error);
36
+ return action;
37
+ }
38
+ function byName(actions) {
39
+ assertOnce("action", actions);
40
+ return Object.fromEntries(actions.map((action) => [action.name, action]));
41
+ }
42
+ /**
43
+ * Every authenticated caller may call a protected action. The contract's identity
44
+ * requirement is enforced independently, including when its handler reads no identity.
45
+ */
46
+ const allowAll = () => Effect.void;
47
+ /**
48
+ * `authorize`, checked: the types require a function, and plain JavaScript can still pass
49
+ * none, which must not serve every caller.
50
+ */
51
+ const assertAuthorizer = (before) => {
52
+ if (!Predicate.isFunction(before)) throw new Error("Missing authorize: pass an authorization function, or Action.allowAll");
53
+ return before;
54
+ };
55
+ /** `authorize`, present wherever a protected action is implemented. */
56
+ const assertAuthorization = (authorize) => {
57
+ if (authorize === void 0) throw new Error("Protected actions require authorize, or Action.allowAll");
58
+ return authorize;
59
+ };
60
+ /** No `authorize` where only public actions are implemented: it would never run. */
61
+ const assertNoAuthorization = (authorize) => {
62
+ if (authorize !== void 0) throw new Error("A public-only target takes no authorize");
63
+ return allowAll;
64
+ };
65
+ /**
66
+ * The layer building `authorize`: a plain function as it is, a built one once its Effect
67
+ * runs, checked when the layer builds, as a builder's record is. `Effect.isEffect` tells them
68
+ * apart, so one written with `Effect.fn` stays plain.
69
+ */
70
+ const authorizerOf = (before) => memoized(Effect.isEffect(before) ? Effect.map(before, assertAuthorizer) : Effect.succeed(assertAuthorizer(before)));
71
+ function implement(actions, handlers, options) {
72
+ const listed = Array$1.ensure(actions);
73
+ const names = listed.map((action) => action.name);
74
+ assertOnce("action", listed);
75
+ const authorizer = authorizerOf(listed.some((action) => action.caller !== Anyone) ? assertAuthorization(options?.authorize) : assertNoAuthorization(options?.authorize));
76
+ const record = (built) => {
77
+ const keyed = Predicate.isFunction(built) ? Object.fromEntries(Array.isArray(actions) ? [] : listed.map(({ name }) => [name, built])) : built;
78
+ assertKnown("handlers", Object.keys(keyed), names);
79
+ const bound = listed.flatMap((action) => {
80
+ const handle = Object.hasOwn(keyed, action.name) ? keyed[action.name] : void 0;
81
+ return Predicate.isFunction(handle) ? [[action, handle]] : [];
82
+ });
83
+ const missing = names.filter((name) => !bound.some(([action]) => action.name === name));
84
+ if (missing.length > 0) throw new Error(`Missing handlers: ${missing.join(", ")}`);
85
+ return bound;
29
86
  };
87
+ const recorded = Effect.isEffect(handlers) ? Effect.map(handlers, record) : Effect.succeed(record(handlers));
88
+ return new Implementation(listed, memoized(recorded), authorizer);
89
+ }
90
+ function layer(implementations) {
91
+ return builders(toList(implementations));
92
+ }
93
+ /**
94
+ * A value through `schema`'s JSON codec, as a wire carries it: encoded to JSON text, then
95
+ * decoded, so what comes out is what a remote call's other side decodes, `-0` as `0`. Every
96
+ * issue is reported.
97
+ */
98
+ const wire = (schema) => {
99
+ const codec = Schema.fromJsonString(Schema.toCodecJson(schema));
100
+ const encode = Schema.encodeUnknownEffect(codec, { errors: "all" });
101
+ const decode = Schema.decodeUnknownEffect(codec, { errors: "all" });
102
+ return (value) => Effect.flatMap(encode(value), (encoded) => decode(encoded));
103
+ };
104
+ /**
105
+ * `cause` with each failure passed through `failure` where it stands, so it keeps its
106
+ * annotations, the span it failed in among them; failed anew, it would lose them. One that
107
+ * passes keeps the stack of where it was made, where the decoded error's would be the codec's.
108
+ * One that does not, such as an error the action does not declare, is a defect: its
109
+ * `SchemaError`, then the failure itself.
110
+ */
111
+ const mapFailures = (cause, failure) => Effect.map(Effect.forEach(cause.reasons, (reason) => Cause.isFailReason(reason) ? Effect.match(failure(reason.error), {
112
+ onFailure: (refused) => Cause.combine(Cause.die(refused), Cause.die(reason.error)),
113
+ onSuccess: (decoded) => Cause.fail(Predicate.isError(reason.error) && Predicate.isError(decoded) ? Object.assign(decoded, { stack: reason.error.stack }) : decoded)
114
+ }).pipe(Effect.map((mapped) => Cause.annotate(mapped, Cause.reasonAnnotations(reason)))) : Effect.succeed(Cause.fromReasons([reason]))), (causes) => Cause.fromReasons(causes.flatMap(({ reasons }) => reasons)));
115
+ /**
116
+ * `action`'s method, running `run`, its handler behind authorization, as a remote
117
+ * call runs: input that does not pass through its codec is `InvalidInput`, and nothing else
118
+ * runs; a success or a failure that does not is a defect, as it is an empty 500 over HTTP,
119
+ * since the handler broke its contract. A failure passes through the codec of
120
+ * every error the action declares, the built-in ones included.
121
+ */
122
+ const methodOf = (action, run) => {
123
+ const input = wire(action.input);
124
+ const success = wire(action.success);
125
+ const failure = wire(Schema.Union(projectedErrors(action)));
126
+ return (...args) => inputOf(action, args).pipe(Effect.flatMap(input), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((value) => Effect.catchCause(run(value), (cause) => Effect.flatMap(mapFailures(cause, failure), Effect.failCause))), Effect.flatMap((value) => Effect.orDie(success(value))));
127
+ };
128
+ function client(served, options) {
129
+ const apps = select(toList(served), options?.actions);
130
+ assertOnce("action", apps.flatMap((app) => app.actions));
131
+ return Effect.map(built(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, methodOf(action, run)])));
30
132
  }
31
133
  //#endregion
32
- export { make };
134
+ export { Anyone, BuiltIn, Forbidden, Image, InvalidInput, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
@@ -1,36 +1,107 @@
1
- import { Action_d_exports, Any, Handler } from "./Action.mjs";
2
- import { Actions } from "./internal/actions.mjs";
3
- import { HandlerContext, Handlers, HandlersContext, Implementation } from "./internal/implementation.mjs";
4
- import { Options as Options$1 } from "./internal/cli.mjs";
5
- import { Effect, Scope } from "effect";
1
+ import { BuiltIn } from "./internal/errors.mjs";
2
+ import { ActionOf, BuildError, BuildServices, Holding, Known, Member, RequestOf, Selected, SelectedOf, Selection, Served } from "./internal/implementation.mjs";
3
+ import { Action_d_exports, Any } from "./Action.mjs";
4
+ import { VerifierError } from "./internal/authentication.mjs";
5
+ import { Options, UserError } from "./internal/cli.mjs";
6
+ import { AnyHttp, Options as Options$2 } from "./internal/client.mjs";
7
+ import { Effect, Schema } from "effect";
8
+ import { HttpClient, HttpClientError } from "effect/http";
6
9
  import { Command } from "effect/cli";
7
10
  //#region src/ActionCli.d.ts
8
11
  /**
9
- * The hook a local command runs before the selected handler. A CLI does not serialize
10
- * failures, so its refusal is a typed failure of the command effect rather than a
11
- * declared surface error; the native parser has already decoded the input.
12
+ * An aggregate command of the actions `A`: its name, the actions that are its subcommands,
13
+ * and their options.
12
14
  */
13
- interface BeforeOptions<E, R> {
14
- readonly before?: (action: Any) => Effect.Effect<void, E, R>;
15
+ interface Options$1<A extends Any = Any> {
16
+ /** The aggregate command's name. */
17
+ readonly name: string;
18
+ /**
19
+ * The actions that are subcommands, among the binding's or the implementations' actions:
20
+ * `[GetUser, RenameUser]`. Defaults to every action of them.
21
+ */
22
+ readonly actions?: ReadonlyArray<A> | undefined;
23
+ /**
24
+ * The options of each subcommand, keyed by its action's name, as `command` takes them:
25
+ * `{ readFile: { positional: ["path"], render } }`.
26
+ */
27
+ readonly commands?: { readonly [K in A as K["name"]]?: Options<K>; };
15
28
  }
16
- /** Parsing, rendering and the pre-handler hook of one local command. */
17
- export type Options<Output, E = never, R = never, Parameters extends Command.Command.Config = never> = Options$1<Output, Parameters> & BeforeOptions<E, R>;
18
- /** Configuration for an aggregate group command. */
19
- export interface GroupOptions<E = never, R = never> extends BeforeOptions<E, R> {
20
- /** Override the group command name. */
21
- readonly name?: string;
22
- }
23
- type Selected<G extends Actions, Name extends G["actions"][number]["name"]> = Extract<G["actions"][number], {
24
- readonly name: Name;
25
- }>;
26
- type BoundHandler<Name extends string, A extends Any, R> = Readonly<Record<Name, Handler<A, R>>>;
27
- /**
28
- * Project one explicitly selected local action into a native Effect CLI command.
29
- * The action name resolves against the bound group; no HTTP fallback exists.
30
- */
31
- export declare const command: <G extends Actions, Name extends G["actions"][number]["name"], H extends BoundHandler<Name, Selected<G, Name>, HandlerContext<H, Name>>, EX, RX, EB = never, RB = never, Parameters extends Command.Command.Config = never>(app: Implementation<G, H, EX, RX>, name: Name, options?: Options<Selected<G, Name>["success"]["Type"], EB, RB, Parameters>) => Command.Command<string, never, {}, EB | EX | import("effect/Schema").SchemaError | Extract<G["actions"][number], {
32
- readonly name: Name;
33
- }>["errors"][number]["Type"], Exclude<RB, Scope.Scope> | Exclude<RX, Scope.Scope> | Exclude<HandlerContext<H, Name>, Scope.Scope>>;
34
- /** Project every local action below its group namespace with default CLI options. */
35
- export declare const group: <G extends Actions, H extends Handlers<HandlersContext<H>>, EX, RX, EB = never, RB = never>(app: Implementation<G, H, EX, RX>, options?: GroupOptions<EB, RB>) => Command.Command<string, {}, {}, EB | EX | import("effect/Schema").SchemaError | G["actions"][number]["errors"][number]["Type"], Exclude<Exclude<RB, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<RX, Scope.Scope>, Command.CommandContext<string>> | Exclude<Exclude<HandlersContext<H>, Scope.Scope>, Command.CommandContext<string>>>;
36
- //#endregion
29
+ /** The `commands` any member of `O` may give. */
30
+ type CommandsOf<O> = O extends unknown ? "commands" extends keyof O ? NonNullable<O["commands" & keyof O]> : never : never;
31
+ /** The keys of any member of `C`. */
32
+ type KeysOf<C> = C extends unknown ? keyof C : never;
33
+ /** The options any member of `C` gives command `K`. */
34
+ type CommandOf<C, K> = C extends unknown ? (K extends keyof C ? C[K] : never) : never;
35
+ /**
36
+ * No option, no command beyond the actions `A` of the target, and no command's option, beyond
37
+ * `Keys`'s, in any member of `O`.
38
+ */
39
+ type KnownOptions<O, Keys, A extends Any> = Known<O, Keys> & ([CommandsOf<O>] extends [never] ? unknown : {
40
+ readonly commands?: Known<CommandsOf<O>, { readonly [K in A["name"]]: Options<A>; }> & { readonly [K in KeysOf<CommandsOf<O>>]?: Known<CommandOf<CommandsOf<O>, K>, Options<Any>>; };
41
+ });
42
+ /** The implementation of `A` among `App`. */
43
+ type Owning<App, A extends Any> = App extends unknown ? A extends ActionOf<App> ? App : never : never;
44
+ /**
45
+ * What one local command of `A` runs: its implementation's builder and authorization, then
46
+ * its handler. Authorization may refuse and any handler fail with a built-in error, so
47
+ * every one fails with `Action.BuiltIn` too.
48
+ */
49
+ type Local<App, A extends Any> = Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuildError<App, A> | BuiltIn, RequestOf<App, A> | BuildServices<App, A>>;
50
+ /**
51
+ * Project one implemented action into a native Effect CLI command that runs its handler in
52
+ * process, behind its implementation's authorization. It is named after the action in kebab
53
+ * case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
54
+ * as JSON when it is not a struct. It needs what the authorization, its handler and its builder
55
+ * need; the host provides the identity. It prints the result on stdout; a failure is a
56
+ * `UserError`, which `Command.run` prints on stderr. `remoteCommand` calls a server instead.
57
+ */
58
+ export declare function command<const Apps extends Served, A extends ActionOf<Member<Apps>>, const O = {}>(implementations: Apps, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, Options<Any>>>): Command.Command<string, never, {}, UserError<Effect.Error<Local<Owning<Member<Apps>, A>, A>>>, Effect.Services<Local<Owning<Member<Apps>, A>, A>>>;
59
+ /**
60
+ * Project one action of an HTTP binding into a native Effect CLI command, derived as `command`
61
+ * derives it, that calls the action over HTTP through its `ActionHttp.client` method, made with
62
+ * the `client` options on the host's `HttpClient`, and fails as the method does. It runs
63
+ * nothing locally.
64
+ */
65
+ export declare function remoteCommand<const H extends AnyHttp, A extends H["actions"][number], const O = {}>(binding: H, action: A, options?: O & NoInfer<RemoteCommandOptions<A>> & NoInfer<Known<O, RemoteCommandOptions<Any>>>): Command.Command<string, never, {}, HttpUserError<H, A>, HttpClient.HttpClient>;
66
+ /** A remote command's options, its action `A`. */
67
+ export type RemoteCommandOptions<A extends Any = Any> = Options<A> & {
68
+ /**
69
+ * Its client's options, as `ActionHttp.client` takes them: `baseUrl`, and
70
+ * `transformClient` for credentials. They reach this command's requests alone.
71
+ */
72
+ readonly client?: Options$2;
73
+ };
74
+ /** A remote aggregate command's options, its actions `A`. */
75
+ export type RemoteOptions<A extends Any = Any> = Options$1<A> & {
76
+ /**
77
+ * Every subcommand's client options, as `ActionHttp.client` takes them: `baseUrl`, and
78
+ * `transformClient` for credentials. They reach this aggregate's requests alone.
79
+ */
80
+ readonly client?: Options$2;
81
+ };
82
+ /** What an aggregate command over binding `H` fails with, running its actions `A`. */
83
+ type HttpUserError<H extends AnyHttp, A extends Any> = UserError<A["error"][number]["Type"] | H["error"][number]["Type"] | VerifierError<A, H["authentication"]>["Type"] | BuiltIn | HttpClientError.HttpClientError | Schema.SchemaError>;
84
+ /**
85
+ * Project every implemented action as a subcommand of one aggregate command, each named after
86
+ * its action in kebab case and run in process. `commands` gives a subcommand the options
87
+ * `command` takes, by action name. `remote` calls a server instead.
88
+ */
89
+ export declare function make<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}>(implementations: Apps, options: O & NoInfer<Options$1<ActionOf<Member<Apps>>>> & NoInfer<KnownOptions<O, Options$1, ActionOf<Member<Apps>>>>): Command.Command<string, {}, {}, UserError<Effect.Error<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>>, Effect.Services<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>>;
90
+ /**
91
+ * Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
92
+ * `make` derives it, each calling its action over HTTP. `client` configures every
93
+ * subcommand's client; `commands` gives a subcommand the options `command` takes.
94
+ */
95
+ export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}>(binding: H, options: O & NoInfer<RemoteOptions<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteOptions<Any>, H["actions"][number]>>): Command.Command<string, {}, {}, HttpUserError<H, Selected<O, H["actions"][number]>>, HttpClient.HttpClient>;
96
+ /**
97
+ * A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
98
+ * subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
99
+ * provided within writes to stderr, and a failure `runMain` would report, such as a defect or
100
+ * a failure of a layer the host provides, is reported on stderr instead, once. A command's
101
+ * own failure, which `Command.run` has printed, is not reported again. The exit code is the
102
+ * one `runMain` gives: the failure's `Runtime.errorExitCode`, 1 by default, or 130 for an
103
+ * interruption.
104
+ */
105
+ export declare const logToStderr: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, never, R>;
106
+ //#endregion
107
+ export { type Options as CommandOptions, Options$1 as Options, type UserError };
@@ -1,26 +1,124 @@
1
- import { selectNamed } from "./internal/actions.mjs";
2
- import { dispatch } from "./internal/implementation.mjs";
1
+ import { assertDistinct, assertKnown } from "./internal/actions.mjs";
2
+ import { Implementation, acquire, assertHeld, select, toList } from "./internal/implementation.mjs";
3
+ import { assertInBinding, methods } from "./internal/client.mjs";
3
4
  import { command as command$1 } from "./internal/cli.mjs";
4
- import { Effect } from "effect";
5
+ import { Cause, Effect, Logger, Runtime } from "effect";
5
6
  import { Command } from "effect/cli";
6
7
  //#region src/ActionCli.ts
7
8
  /**
8
- * Acquire the implementation in a scope of its own, run one action through the hook
9
- * and its handler, and release it: every local command, selected or grouped.
9
+ * Build the implementation's handlers for this call alone, run one action through its
10
+ * authorization and handler, and release them: every local command, selected or aggregated.
11
+ * The call's own scope, which every call has, closes first, so its finalizers run while the
12
+ * builder's resources are still open; a local build never reuses one the host already made.
10
13
  */
11
- const local = (app, action, before, input) => Effect.scoped(Effect.flatMap(app.build, (handlers) => dispatch(app.group, action, handlers, before)(input)));
14
+ const local = (app, action, input) => {
15
+ const own = Implementation.share([action], app);
16
+ return Effect.flatMap(acquire([own]), (bound) => {
17
+ const [, run] = bound.find(([candidate]) => candidate === action) ?? [];
18
+ return run === void 0 ? Effect.die(/* @__PURE__ */ new Error(`No handler for ${action.name}`)) : run(input);
19
+ }).pipe(Effect.provide(Implementation.layerOf(own), { local: true }));
20
+ };
12
21
  /**
13
- * Project one explicitly selected local action into a native Effect CLI command.
14
- * The action name resolves against the bound group; no HTTP fallback exists.
22
+ * The one implementation of `action` among `apps`. One implemented twice is refused rather
23
+ * than the first run; other actions' names are not this command's to check.
15
24
  */
16
- const command = (app, name, options) => {
17
- const action = selectNamed(app.group.actions, name, `action "${app.group.name}.${name}"`);
18
- return command$1(action, (input) => local(app, action, options?.before, input), options);
25
+ const implementationOf = (apps, action) => {
26
+ const [app, ...others] = apps.filter((candidate) => candidate.actions.includes(action));
27
+ if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here`);
28
+ if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here`);
29
+ return app;
19
30
  };
20
- /** Project every local action below its group namespace with default CLI options. */
21
- const group = (app, options) => {
22
- const commands = app.group.actions.map((action) => command$1(action, (input) => local(app, action, options?.before, input)));
23
- return Command.make(options?.name ?? app.group.name).pipe(Command.withSubcommands(commands));
31
+ /** One command running `action` in process, by its one implementation among `apps`. */
32
+ const inProcess = (apps, action, options) => {
33
+ const app = implementationOf(apps, action);
34
+ return command$1(action, (input) => local(app, action, input), options);
24
35
  };
36
+ /**
37
+ * One command calling `action` through the binding's client, made with `client` on the
38
+ * host's `HttpClient`, whose failures include the binding's errors.
39
+ */
40
+ const overHttp = (binding, action, options, client) => {
41
+ assertInBinding(binding.actions, action);
42
+ return command$1(action, (input) => Effect.flatMap(methods(binding, client), (methodOf) => methodOf(action)(input)), options, binding.error);
43
+ };
44
+ /**
45
+ * One aggregate command named `name`, a subcommand per action of `actions`, each made by
46
+ * `project` with its options in `commands`: a local aggregate's and a remote one's.
47
+ */
48
+ const aggregate = (name, actions, commands, project) => {
49
+ const subcommands = actions.map((action) => ({
50
+ action,
51
+ command: project(action, Object.hasOwn(commands, action.name) ? commands[action.name] : void 0)
52
+ }));
53
+ assertDistinct("command", subcommands, ({ command }) => command.name, ({ action }) => `action ${action.name}`);
54
+ return Command.make(name).pipe(Command.withSubcommands(subcommands.map(({ command }) => command)));
55
+ };
56
+ /** A command of any action of the target, so one record serves several selections. */
57
+ const commandsOf = (commands, actions) => {
58
+ const given = commands ?? {};
59
+ assertKnown("commands", Object.keys(given), actions.map((action) => action.name));
60
+ return given;
61
+ };
62
+ /**
63
+ * Project one implemented action into a native Effect CLI command that runs its handler in
64
+ * process, behind its implementation's authorization. It is named after the action in kebab
65
+ * case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
66
+ * as JSON when it is not a struct. It needs what the authorization, its handler and its builder
67
+ * need; the host provides the identity. It prints the result on stdout; a failure is a
68
+ * `UserError`, which `Command.run` prints on stderr. `remoteCommand` calls a server instead.
69
+ */
70
+ function command(implementations, action, options) {
71
+ return inProcess(toList(implementations), action, options);
72
+ }
73
+ /**
74
+ * Project one action of an HTTP binding into a native Effect CLI command, derived as `command`
75
+ * derives it, that calls the action over HTTP through its `ActionHttp.client` method, made with
76
+ * the `client` options on the host's `HttpClient`, and fails as the method does. It runs
77
+ * nothing locally.
78
+ */
79
+ function remoteCommand(binding, action, options) {
80
+ const { client, ...syntax } = options ?? {};
81
+ return overHttp(binding, action, options === void 0 ? void 0 : syntax, client);
82
+ }
83
+ /**
84
+ * Project every implemented action as a subcommand of one aggregate command, each named after
85
+ * its action in kebab case and run in process. `commands` gives a subcommand the options
86
+ * `command` takes, by action name. `remote` calls a server instead.
87
+ */
88
+ function make(implementations, options) {
89
+ const apps = toList(implementations);
90
+ const commands = commandsOf(options.commands, apps.flatMap((app) => app.actions));
91
+ const served = select(apps, options.actions);
92
+ return aggregate(options.name, served.flatMap((app) => app.actions), commands, (action, syntax) => inProcess(served, action, syntax));
93
+ }
94
+ /**
95
+ * Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
96
+ * `make` derives it, each calling its action over HTTP. `client` configures every
97
+ * subcommand's client; `commands` gives a subcommand the options `command` takes.
98
+ */
99
+ function remote(binding, options) {
100
+ const listed = options.actions;
101
+ if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
102
+ const commands = commandsOf(options.commands, binding.actions);
103
+ return aggregate(options.name, binding.actions.filter((action) => listed?.includes(action) ?? true), commands, (action, syntax) => overHttp(binding, action, syntax, options.client));
104
+ }
105
+ /**
106
+ * A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
107
+ * subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
108
+ * provided within writes to stderr, and a failure `runMain` would report, such as a defect or
109
+ * a failure of a layer the host provides, is reported on stderr instead, once. A command's
110
+ * own failure, which `Command.run` has printed, is not reported again. The exit code is the
111
+ * one `runMain` gives: the failure's `Runtime.errorExitCode`, 1 by default, or 130 for an
112
+ * interruption.
113
+ */
114
+ const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
115
+ if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
116
+ const squashed = Cause.squash(cause);
117
+ const reported = Object.assign(new Error("Reported on stderr", { cause }), {
118
+ [Runtime.errorReported]: false,
119
+ [Runtime.errorExitCode]: Runtime.getErrorExitCode(squashed)
120
+ });
121
+ return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reported));
122
+ }), Effect.provideService(Logger.LogToStderr, true));
25
123
  //#endregion
26
- export { command, group };
124
+ export { command, logToStderr, make, remote, remoteCommand };