@gjermundgaraba/effect-actions 0.8.0 → 0.10.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 +57 -50
- package/dist/Action.d.mts +285 -79
- package/dist/Action.mjs +124 -23
- package/dist/ActionCli.d.mts +102 -31
- package/dist/ActionCli.mjs +114 -16
- package/dist/ActionHttp.d.mts +170 -51
- package/dist/ActionHttp.mjs +130 -79
- package/dist/ActionMcp.d.mts +71 -21
- package/dist/ActionMcp.mjs +108 -81
- package/dist/ActionToolkit.d.mts +56 -27
- package/dist/ActionToolkit.mjs +16 -5
- package/dist/Authentication.d.mts +129 -33
- package/dist/Authentication.mjs +231 -38
- package/dist/Testing.d.mts +82 -57
- package/dist/Testing.mjs +89 -79
- package/dist/internal/actions.d.mts +5 -25
- package/dist/internal/actions.mjs +75 -27
- package/dist/internal/authentication.d.mts +102 -0
- package/dist/internal/authentication.mjs +11 -0
- package/dist/internal/call.d.mts +12 -0
- package/dist/internal/call.mjs +12 -0
- package/dist/internal/cli.d.mts +36 -22
- package/dist/internal/cli.mjs +311 -33
- package/dist/internal/client.d.mts +43 -0
- package/dist/internal/client.mjs +25 -0
- package/dist/internal/console.mjs +77 -0
- package/dist/internal/declared.d.mts +13 -0
- package/dist/internal/declared.mjs +34 -0
- package/dist/internal/errors.d.mts +77 -0
- package/dist/internal/errors.mjs +79 -0
- package/dist/internal/implementation.d.mts +171 -41
- package/dist/internal/implementation.mjs +147 -29
- package/dist/internal/mcp.d.mts +11 -0
- package/dist/internal/mcp.mjs +52 -0
- package/dist/internal/memory.d.mts +6 -0
- package/dist/internal/memory.mjs +38 -0
- package/dist/internal/refusal.mjs +67 -0
- package/dist/internal/schema-errors.mjs +20 -0
- package/dist/internal/tools.mjs +27 -57
- package/docs/Action.md +401 -73
- package/docs/ActionCli.md +280 -51
- package/docs/ActionHttp.md +266 -82
- package/docs/ActionMcp.md +149 -104
- package/docs/ActionToolkit.md +97 -35
- package/docs/Authentication.md +369 -62
- package/docs/CONTEXT.md +23 -36
- package/docs/README.md +29 -73
- package/docs/Testing.md +154 -77
- package/docs/guarantees.md +81 -43
- package/docs/setup.md +90 -27
- package/package.json +9 -17
- package/dist/ActionCatalog.d.mts +0 -30
- package/dist/ActionCatalog.mjs +0 -39
- package/dist/ActionCliClient.d.mts +0 -48
- package/dist/ActionCliClient.mjs +0 -43
- package/dist/ActionGroup.d.mts +0 -44
- package/dist/ActionGroup.mjs +0 -35
- package/dist/ActionHttpClient.d.mts +0 -33
- package/dist/ActionHttpClient.mjs +0 -16
- package/dist/TestingClient.d.mts +0 -17
- package/dist/TestingClient.mjs +0 -22
- package/dist/internal/fetchClient.d.mts +0 -6
- package/dist/internal/fetchClient.mjs +0 -8
- package/dist/internal/tools.d.mts +0 -22
- package/docs/ActionCatalog.md +0 -54
- package/docs/ActionCliClient.md +0 -63
- package/docs/ActionGroup.md +0 -128
- package/docs/ActionHttpClient.md +0 -80
package/dist/Action.mjs
CHANGED
|
@@ -1,32 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 { Array as Array$1, Cause, Context, Effect, Predicate, Schema } from "effect";
|
|
3
6
|
//#region src/Action.ts
|
|
4
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* An object without fields: an action without arguments, `{}` given or not. Effect's own
|
|
9
|
+
* `Tool.EmptyParams`: strict, unlike `Schema.Struct({})`, which accepts any value but `null`,
|
|
10
|
+
* and the object root MCP requires.
|
|
11
|
+
*/
|
|
5
12
|
const NoInput = Schema.Record(Schema.String, Schema.Never);
|
|
13
|
+
/**
|
|
14
|
+
* The schema an option stands for: a schema as given, `NoInput` for no fields, or the struct
|
|
15
|
+
* of the fields, which may be keyed by symbols, as in a struct.
|
|
16
|
+
*/
|
|
17
|
+
const codecOf = (schema) => Schema.isSchema(schema) ? schema : Reflect.ownKeys(schema).length === 0 ? NoInput : Schema.Struct(schema);
|
|
6
18
|
function make(name, options) {
|
|
7
19
|
assertName("action name", name);
|
|
8
|
-
const {
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
20
|
+
const { caller, readOnly } = options;
|
|
21
|
+
if (caller !== Anyone && !Context.isKey(caller)) throw new Error("Missing caller: declare Action.Anyone or an identity service key");
|
|
22
|
+
if (caller !== Anyone && Context.isReference(caller)) throw new Error("Invalid caller: an identity is a Context.Service, not a Context.Reference");
|
|
23
|
+
if (readOnly !== true && readOnly !== false) throw new Error(`Invalid readOnly: ${String(readOnly)}`);
|
|
24
|
+
const action = {
|
|
25
|
+
caller,
|
|
22
26
|
name,
|
|
23
27
|
description: options.description,
|
|
24
|
-
input: options.input ??
|
|
25
|
-
success: options.success,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
mcp
|
|
28
|
+
input: codecOf(options.input ?? {}),
|
|
29
|
+
success: options.success === void 0 ? Schema.Void : codecOf(options.success),
|
|
30
|
+
error: errorList(options.error),
|
|
31
|
+
readOnly,
|
|
32
|
+
mcp: { ...options.mcp }
|
|
33
|
+
};
|
|
34
|
+
assertOwnTags(`Action "${action.name}"`, action.error);
|
|
35
|
+
return action;
|
|
36
|
+
}
|
|
37
|
+
function byName(actions) {
|
|
38
|
+
assertOnce("action", actions);
|
|
39
|
+
return Object.fromEntries(actions.map((action) => [action.name, action]));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every authenticated caller may call a protected action. The contract's identity
|
|
43
|
+
* requirement is enforced independently, including when its handler reads no identity.
|
|
44
|
+
*/
|
|
45
|
+
const allowAll = () => Effect.void;
|
|
46
|
+
/**
|
|
47
|
+
* `authorize`, checked: the types require a function, and plain JavaScript can still pass
|
|
48
|
+
* none, which must not serve every caller.
|
|
49
|
+
*/
|
|
50
|
+
const assertAuthorizer = (before) => {
|
|
51
|
+
if (!Predicate.isFunction(before)) throw new Error("Missing authorize: pass an authorization function, or Action.allowAll");
|
|
52
|
+
return before;
|
|
53
|
+
};
|
|
54
|
+
/** `authorize`, present wherever a protected action is implemented. */
|
|
55
|
+
const assertAuthorization = (authorize) => {
|
|
56
|
+
if (authorize === void 0) throw new Error("Protected actions require authorize, or Action.allowAll");
|
|
57
|
+
return authorize;
|
|
58
|
+
};
|
|
59
|
+
/** No `authorize` where only public actions are implemented: it would never run. */
|
|
60
|
+
const assertNoAuthorization = (authorize) => {
|
|
61
|
+
if (authorize !== void 0) throw new Error("A public-only target takes no authorize");
|
|
62
|
+
return allowAll;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The layer building `authorize`: a plain function as it is, a built one once its Effect
|
|
66
|
+
* runs, checked when the layer builds, as a builder's record is. `Effect.isEffect` tells them
|
|
67
|
+
* apart, so one written with `Effect.fn` stays plain.
|
|
68
|
+
*/
|
|
69
|
+
const authorizerOf = (before) => memoized(Effect.isEffect(before) ? Effect.map(before, assertAuthorizer) : Effect.succeed(assertAuthorizer(before)));
|
|
70
|
+
function implement(actions, handlers, options) {
|
|
71
|
+
const listed = Array$1.ensure(actions);
|
|
72
|
+
const names = listed.map((action) => action.name);
|
|
73
|
+
assertOnce("action", listed);
|
|
74
|
+
const authorizer = authorizerOf(listed.some((action) => action.caller !== Anyone) ? assertAuthorization(options?.authorize) : assertNoAuthorization(options?.authorize));
|
|
75
|
+
const record = (built) => {
|
|
76
|
+
const keyed = Predicate.isFunction(built) ? Object.fromEntries(Array.isArray(actions) ? [] : listed.map(({ name }) => [name, built])) : built;
|
|
77
|
+
assertKnown("handlers", Object.keys(keyed), names);
|
|
78
|
+
const bound = listed.flatMap((action) => {
|
|
79
|
+
const handle = Object.hasOwn(keyed, action.name) ? keyed[action.name] : void 0;
|
|
80
|
+
return Predicate.isFunction(handle) ? [[action, handle]] : [];
|
|
81
|
+
});
|
|
82
|
+
const missing = names.filter((name) => !bound.some(([action]) => action.name === name));
|
|
83
|
+
if (missing.length > 0) throw new Error(`Missing handlers: ${missing.join(", ")}`);
|
|
84
|
+
return bound;
|
|
29
85
|
};
|
|
86
|
+
const recorded = Effect.isEffect(handlers) ? Effect.map(handlers, record) : Effect.succeed(record(handlers));
|
|
87
|
+
return new Implementation(listed, memoized(recorded), authorizer);
|
|
88
|
+
}
|
|
89
|
+
function layer(implementations) {
|
|
90
|
+
return builders(toList(implementations));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A value through `schema`'s JSON codec, as a wire carries it: encoded to JSON text, then
|
|
94
|
+
* decoded, so what comes out is what a remote call's other side decodes, `-0` as `0`. Every
|
|
95
|
+
* issue is reported.
|
|
96
|
+
*/
|
|
97
|
+
const wire = (schema) => {
|
|
98
|
+
const codec = Schema.fromJsonString(Schema.toCodecJson(schema));
|
|
99
|
+
const encode = Schema.encodeUnknownEffect(codec, { errors: "all" });
|
|
100
|
+
const decode = Schema.decodeUnknownEffect(codec, { errors: "all" });
|
|
101
|
+
return (value) => Effect.flatMap(encode(value), (encoded) => decode(encoded));
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* `cause` with each failure passed through `failure` where it stands, so it keeps its
|
|
105
|
+
* annotations, the span it failed in among them; failed anew, it would lose them. One that
|
|
106
|
+
* passes keeps the stack of where it was made, where the decoded error's would be the codec's.
|
|
107
|
+
* One that does not, such as an error the action does not declare, is a defect: its
|
|
108
|
+
* `SchemaError`, then the failure itself.
|
|
109
|
+
*/
|
|
110
|
+
const mapFailures = (cause, failure) => Effect.map(Effect.forEach(cause.reasons, (reason) => Cause.isFailReason(reason) ? Effect.match(failure(reason.error), {
|
|
111
|
+
onFailure: (refused) => Cause.combine(Cause.die(refused), Cause.die(reason.error)),
|
|
112
|
+
onSuccess: (decoded) => Cause.fail(Predicate.isError(reason.error) && Predicate.isError(decoded) ? Object.assign(decoded, { stack: reason.error.stack }) : decoded)
|
|
113
|
+
}).pipe(Effect.map((mapped) => Cause.annotate(mapped, Cause.reasonAnnotations(reason)))) : Effect.succeed(Cause.fromReasons([reason]))), (causes) => Cause.fromReasons(causes.flatMap(({ reasons }) => reasons)));
|
|
114
|
+
/**
|
|
115
|
+
* `action`'s method, running `run`, its handler behind authorization, as a remote
|
|
116
|
+
* call runs: input that does not pass through its codec is `InvalidInput`, and nothing else
|
|
117
|
+
* runs; a success or a failure that does not is a defect, as it is an empty 500 over HTTP,
|
|
118
|
+
* since the handler broke its contract. A failure passes through the codec of
|
|
119
|
+
* every error the action declares, the built-in ones included.
|
|
120
|
+
*/
|
|
121
|
+
const methodOf = (action, run) => {
|
|
122
|
+
const input = wire(action.input);
|
|
123
|
+
const success = wire(action.success);
|
|
124
|
+
const failure = wire(Schema.Union(projectedErrors(action)));
|
|
125
|
+
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))));
|
|
126
|
+
};
|
|
127
|
+
function client(served, options) {
|
|
128
|
+
const apps = select(toList(served), options?.actions);
|
|
129
|
+
assertOnce("action", apps.flatMap((app) => app.actions));
|
|
130
|
+
return Effect.map(built(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, methodOf(action, run)])));
|
|
30
131
|
}
|
|
31
132
|
//#endregion
|
|
32
|
-
export { make };
|
|
133
|
+
export { Anyone, BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, allowAll, byName, client, implement, layer, make };
|
package/dist/ActionCli.d.mts
CHANGED
|
@@ -1,36 +1,107 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
14
|
-
|
|
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
|
-
/**
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 };
|
package/dist/ActionCli.mjs
CHANGED
|
@@ -1,26 +1,124 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
9
|
-
* and
|
|
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,
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
|
17
|
-
const
|
|
18
|
-
|
|
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
|
-
/**
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
return
|
|
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,
|
|
124
|
+
export { command, logToStderr, make, remote, remoteCommand };
|