@gjermundgaraba/effect-actions 0.11.0 → 0.12.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 (215) hide show
  1. package/README.md +101 -40
  2. package/dist/Action.d.mts +29 -117
  3. package/dist/Action.mjs +14 -53
  4. package/dist/ActionCli.d.mts +60 -28
  5. package/dist/ActionCli.mjs +54 -48
  6. package/dist/ActionHttp.d.mts +18 -67
  7. package/dist/ActionHttp.mjs +20 -70
  8. package/dist/ActionMcp.d.mts +23 -29
  9. package/dist/ActionMcp.mjs +56 -114
  10. package/dist/ActionRpc.d.mts +147 -0
  11. package/dist/ActionRpc.mjs +113 -0
  12. package/dist/ActionToolkit.d.mts +5 -8
  13. package/dist/ActionToolkit.mjs +3 -3
  14. package/dist/Authentication.d.mts +15 -15
  15. package/dist/Authentication.mjs +42 -96
  16. package/dist/Testing.d.mts +28 -23
  17. package/dist/Testing.mjs +27 -44
  18. package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
  19. package/dist/authentication/provider.mjs +12 -0
  20. package/dist/authentication/refusal.mjs +36 -0
  21. package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
  22. package/dist/cli/commands.mjs +302 -0
  23. package/dist/{internal → contract}/call.d.mts +2 -2
  24. package/dist/contract/call.mjs +10 -0
  25. package/dist/{internal → contract}/errors.d.mts +13 -10
  26. package/dist/{internal → contract}/errors.mjs +9 -14
  27. package/dist/{internal → contract}/implementation.d.mts +37 -21
  28. package/dist/{internal → contract}/implementation.mjs +18 -49
  29. package/dist/contract/media.d.mts +13 -0
  30. package/dist/contract/media.mjs +18 -0
  31. package/dist/contract/rules.d.mts +25 -0
  32. package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
  33. package/dist/{internal → http}/client.d.mts +7 -9
  34. package/dist/http/client.mjs +17 -0
  35. package/dist/http/declared.mjs +20 -0
  36. package/dist/http/schema-errors.mjs +9 -0
  37. package/dist/mcp/protocol.d.mts +17 -0
  38. package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
  39. package/dist/stdio/console.mjs +64 -0
  40. package/dist/{internal → testing}/memory.d.mts +1 -1
  41. package/dist/{internal → testing}/memory.mjs +5 -17
  42. package/dist/toolkit/tools.mjs +29 -0
  43. package/docs/Action-failures.md +47 -0
  44. package/docs/Action-in-process.md +93 -0
  45. package/docs/Action.md +206 -426
  46. package/docs/ActionCli-failures.md +102 -0
  47. package/docs/ActionCli.md +206 -293
  48. package/docs/ActionHttp-failures.md +83 -0
  49. package/docs/ActionHttp.md +202 -282
  50. package/docs/ActionMcp-failures.md +31 -0
  51. package/docs/ActionMcp.md +222 -271
  52. package/docs/ActionRpc-failures.md +21 -0
  53. package/docs/ActionRpc.md +178 -0
  54. package/docs/ActionToolkit-failures.md +15 -0
  55. package/docs/ActionToolkit.md +100 -119
  56. package/docs/Authentication-advanced.md +139 -0
  57. package/docs/Authentication-failures.md +99 -0
  58. package/docs/Authentication.md +142 -392
  59. package/docs/CONTEXT.md +39 -24
  60. package/docs/README.md +71 -35
  61. package/docs/Testing-failures.md +20 -0
  62. package/docs/Testing.md +186 -151
  63. package/docs/guarantees.md +35 -96
  64. package/docs/setup-failures.md +34 -0
  65. package/docs/setup.md +87 -91
  66. package/examples/README.md +68 -0
  67. package/examples/assistant/README.md +92 -0
  68. package/examples/assistant/src/agent.ts +92 -0
  69. package/examples/assistant/src/cli.ts +34 -0
  70. package/examples/assistant/src/commands.ts +25 -0
  71. package/examples/assistant/src/identity/Identity.ts +33 -0
  72. package/examples/assistant/src/identity/authorize.ts +24 -0
  73. package/examples/assistant/src/identity/handlers.ts +10 -0
  74. package/examples/assistant/src/identity/members.ts +9 -0
  75. package/examples/assistant/src/identity/permissions.ts +23 -0
  76. package/examples/assistant/src/scripted.ts +23 -0
  77. package/examples/assistant/src/tasks/Tasks.ts +63 -0
  78. package/examples/assistant/src/tasks/handlers.ts +25 -0
  79. package/examples/assistant/src/tasks/store.ts +96 -0
  80. package/examples/assistant/src/toolkit.ts +30 -0
  81. package/examples/assistant/test/commands.test.ts +57 -0
  82. package/examples/assistant/test/identity/identity.test.ts +43 -0
  83. package/examples/assistant/test/tasks/store.test.ts +48 -0
  84. package/examples/assistant/test/tasks/tasks.test.ts +75 -0
  85. package/examples/assistant/test/toolkit.test.ts +121 -0
  86. package/examples/kitchen-sink/README.md +317 -0
  87. package/examples/kitchen-sink/src/Actions.ts +19 -0
  88. package/examples/kitchen-sink/src/Api.ts +24 -0
  89. package/examples/kitchen-sink/src/Rpc.ts +12 -0
  90. package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
  91. package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
  92. package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
  93. package/examples/kitchen-sink/src/admin/grants.ts +22 -0
  94. package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
  95. package/examples/kitchen-sink/src/admin/keys.ts +8 -0
  96. package/examples/kitchen-sink/src/agent.ts +47 -0
  97. package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
  98. package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
  99. package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
  100. package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
  101. package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
  102. package/examples/kitchen-sink/src/attachments/download.ts +49 -0
  103. package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
  104. package/examples/kitchen-sink/src/attachments/store.ts +58 -0
  105. package/examples/kitchen-sink/src/browser.ts +33 -0
  106. package/examples/kitchen-sink/src/cli.ts +85 -0
  107. package/examples/kitchen-sink/src/config.ts +18 -0
  108. package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
  109. package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
  110. package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
  111. package/examples/kitchen-sink/src/identity/directory.ts +52 -0
  112. package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
  113. package/examples/kitchen-sink/src/identity/members.ts +13 -0
  114. package/examples/kitchen-sink/src/identity/quota.ts +62 -0
  115. package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
  116. package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
  117. package/examples/kitchen-sink/src/issues/guide.ts +37 -0
  118. package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
  119. package/examples/kitchen-sink/src/issues/store.ts +96 -0
  120. package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
  121. package/examples/kitchen-sink/src/memory.ts +30 -0
  122. package/examples/kitchen-sink/src/ops.ts +42 -0
  123. package/examples/kitchen-sink/src/request-policy.ts +63 -0
  124. package/examples/kitchen-sink/src/routes.ts +120 -0
  125. package/examples/kitchen-sink/src/sdk.ts +46 -0
  126. package/examples/kitchen-sink/src/seed.ts +65 -0
  127. package/examples/kitchen-sink/src/server.ts +62 -0
  128. package/examples/kitchen-sink/src/stdio.ts +34 -0
  129. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
  130. package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
  131. package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
  132. package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
  133. package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
  134. package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
  135. package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
  136. package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
  137. package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
  138. package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
  139. package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
  140. package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
  141. package/examples/kitchen-sink/test/routes.test.ts +123 -0
  142. package/examples/kitchen-sink/test/support/scripted.ts +40 -0
  143. package/examples/live-rpc/README.md +107 -0
  144. package/examples/live-rpc/src/Rpc.ts +8 -0
  145. package/examples/live-rpc/src/browser.ts +98 -0
  146. package/examples/live-rpc/src/chat/Chat.ts +75 -0
  147. package/examples/live-rpc/src/chat/authorize.ts +16 -0
  148. package/examples/live-rpc/src/chat/handlers.ts +50 -0
  149. package/examples/live-rpc/src/chat/room.ts +23 -0
  150. package/examples/live-rpc/src/chat/store.ts +54 -0
  151. package/examples/live-rpc/src/client.ts +37 -0
  152. package/examples/live-rpc/src/identity/Identity.ts +19 -0
  153. package/examples/live-rpc/src/identity/members.ts +8 -0
  154. package/examples/live-rpc/src/identity/verifier.ts +23 -0
  155. package/examples/live-rpc/src/routes.ts +22 -0
  156. package/examples/live-rpc/src/server.ts +43 -0
  157. package/examples/live-rpc/test/browser.test.ts +105 -0
  158. package/examples/live-rpc/test/chat/chat.test.ts +69 -0
  159. package/examples/live-rpc/test/routes.test.ts +99 -0
  160. package/examples/live-rpc/test/server.test.ts +75 -0
  161. package/examples/mcp-tools/README.md +136 -0
  162. package/examples/mcp-tools/src/instructions.ts +12 -0
  163. package/examples/mcp-tools/src/printers/Printers.ts +119 -0
  164. package/examples/mcp-tools/src/printers/fake.ts +115 -0
  165. package/examples/mcp-tools/src/printers/farm.ts +136 -0
  166. package/examples/mcp-tools/src/printers/handlers.ts +29 -0
  167. package/examples/mcp-tools/src/routes.ts +52 -0
  168. package/examples/mcp-tools/src/server.ts +30 -0
  169. package/examples/mcp-tools/src/stdio.ts +26 -0
  170. package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
  171. package/examples/mcp-tools/test/routes.test.ts +202 -0
  172. package/examples/notes-service/README.md +108 -0
  173. package/examples/notes-service/src/Api.ts +13 -0
  174. package/examples/notes-service/src/browser.ts +41 -0
  175. package/examples/notes-service/src/cli.ts +22 -0
  176. package/examples/notes-service/src/commands.ts +89 -0
  177. package/examples/notes-service/src/identity/Identity.ts +31 -0
  178. package/examples/notes-service/src/identity/authorize.ts +18 -0
  179. package/examples/notes-service/src/identity/handlers.ts +10 -0
  180. package/examples/notes-service/src/identity/users.ts +9 -0
  181. package/examples/notes-service/src/identity/verifier.ts +36 -0
  182. package/examples/notes-service/src/notes/Notes.ts +91 -0
  183. package/examples/notes-service/src/notes/handlers.ts +33 -0
  184. package/examples/notes-service/src/notes/store.ts +73 -0
  185. package/examples/notes-service/src/routes.ts +45 -0
  186. package/examples/notes-service/src/server.ts +26 -0
  187. package/examples/notes-service/src/status/Status.ts +10 -0
  188. package/examples/notes-service/src/status/handlers.ts +14 -0
  189. package/examples/notes-service/src/stdio.ts +25 -0
  190. package/examples/notes-service/test/commands.test.ts +134 -0
  191. package/examples/notes-service/test/notes/notes.test.ts +103 -0
  192. package/examples/notes-service/test/routes.test.ts +84 -0
  193. package/examples/quickstart/README.md +31 -0
  194. package/examples/quickstart/src/Api.ts +5 -0
  195. package/examples/quickstart/src/client.ts +13 -0
  196. package/examples/quickstart/src/greeting/Greeting.ts +12 -0
  197. package/examples/quickstart/src/greeting/handlers.ts +6 -0
  198. package/examples/quickstart/src/routes.ts +11 -0
  199. package/examples/quickstart/src/server.ts +16 -0
  200. package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
  201. package/package.json +23 -5
  202. package/dist/internal/actions.d.mts +0 -9
  203. package/dist/internal/authentication.mjs +0 -11
  204. package/dist/internal/call.mjs +0 -12
  205. package/dist/internal/cli.mjs +0 -328
  206. package/dist/internal/client.mjs +0 -25
  207. package/dist/internal/console.mjs +0 -77
  208. package/dist/internal/declared.d.mts +0 -13
  209. package/dist/internal/declared.mjs +0 -34
  210. package/dist/internal/mcp.d.mts +0 -14
  211. package/dist/internal/media.d.mts +0 -8
  212. package/dist/internal/media.mjs +0 -26
  213. package/dist/internal/refusal.mjs +0 -67
  214. package/dist/internal/schema-errors.mjs +0 -20
  215. package/dist/internal/tools.mjs +0 -44
package/dist/Testing.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { Refusal } from "./internal/errors.mjs";
2
- import { assertOnce, projectedErrors } from "./internal/actions.mjs";
3
- import { inputOf } from "./internal/call.mjs";
4
- import { defaultPath, liftOf, statelessRequest } from "./internal/mcp.mjs";
5
- import { clientOf } from "./internal/memory.mjs";
6
- import { Context, Effect, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
7
- import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
1
+ import { Refusal } from "./contract/errors.mjs";
2
+ import { assertOnce, projectedErrors } from "./contract/rules.mjs";
3
+ import { checked } from "./contract/call.mjs";
4
+ import { defaultPath, liftOf, statelessRequest } from "./mcp/protocol.mjs";
5
+ import { clientOf } from "./testing/memory.mjs";
6
+ import { Cause, Context, Effect, ErrorReporter, FileSystem, Layer, Option, Path, Predicate, Schema, identity } from "effect";
8
7
  import { McpSchema } from "effect/ai";
8
+ import { Etag, HttpClient, HttpClientRequest, HttpEffect, HttpPlatform, HttpRouter } from "effect/http";
9
9
  import { Base64, Sse } from "effect/encoding";
10
- //#region src/Testing.ts
10
+ //#region src/testing/Testing.ts
11
11
  function layer(routes) {
12
12
  if (!Layer.isLayer(routes)) return clientOf(routes);
13
13
  return Layer.unwrap(Effect.gen(function* () {
@@ -15,27 +15,24 @@ function layer(routes) {
15
15
  const platform = yield* Layer.build(Layer.fresh(Layer.mergeAll(HttpPlatform.layer, Path.layer, Etag.layerWeak).pipe(Layer.provideMerge(Layer.succeed(FileSystem.FileSystem, fileSystem)))));
16
16
  const context = Context.merge(platform, yield* Effect.context());
17
17
  const app = yield* HttpRouter.toHttpEffect(routes).pipe(Effect.provideContext(context));
18
- return clientOf(HttpEffect.toWebHandlerWith(context)(app));
18
+ return clientOf(HttpEffect.toWebHandlerWith(context)(Effect.tapCause(app, (cause) => isReported(cause) ? Effect.logError(cause) : Effect.void)));
19
19
  }));
20
20
  }
21
+ const isReported = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) && !ErrorReporter.isIgnored(reason.error) || Cause.isDieReason(reason) && !ErrorReporter.isIgnored(reason.defect));
21
22
  /**
22
- * An answer a client method cannot decode as the action's success or a declared error,
23
- * such as the native server's message for invalid arguments or an unknown tool. Its message
24
- * holds the answer.
23
+ * An answer a client method cannot read as the action's success or a declared error, such as
24
+ * the native server's message for invalid arguments or an unknown tool, or a result without
25
+ * its structured content. Its message holds the answer. Structured content that does not
26
+ * decode as the success is a `SchemaError` instead, and a request that fails an
27
+ * `HttpClientError`.
25
28
  */
26
29
  var McpCallError = class extends Schema.TaggedError()("McpCallError", { message: Schema.String }) {};
27
- /** The text of a block, none of a block of another kind. */
28
30
  const textOf = (block) => block?.type === "text" ? block.text : void 0;
29
- /** The JSON-RPC response to a `tools/call`: the native tool result, or a protocol error. */
30
- const ToolReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
31
+ const ToolCallReply = Schema.Union([Schema.Struct({ result: McpSchema.CallToolResult }), Schema.Struct({ error: Schema.Struct({
31
32
  code: Schema.Finite,
32
33
  message: Schema.String
33
34
  }) })]);
34
- const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolReply));
35
- /**
36
- * The reply in a response: its body when it is one JSON message, or the last reply among an
37
- * event stream's events, which notifications may precede.
38
- */
35
+ const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(ToolCallReply));
39
36
  const replyOf = (response, text) => {
40
37
  if (!(response.headers["content-type"] ?? "").startsWith("text/event-stream")) return decodeReply(text);
41
38
  const data = [];
@@ -44,40 +41,25 @@ const replyOf = (response, text) => {
44
41
  }).feed(text);
45
42
  return Option.fromNullishOr(data.flatMap((message) => Option.toArray(decodeReply(message))).at(-1));
46
43
  };
47
- /**
48
- * The JSON of the media `field` a tool result's blocks hold: those of its kind naming it, or
49
- * none, for the whole success, in order; an array, or else the one, if any. Surplus blocks for
50
- * one value stay an array, which its decoding refuses.
51
- */
52
- const mediaOf = ({ name, kind, many }, result) => {
44
+ const mediaFromBlocks = ({ name, kind, many }, result) => {
53
45
  const values = result.content.flatMap((block) => block.type === kind && block._meta?.["effect-actions/field"] === name ? [{
54
46
  data: Base64.encode(block.data),
55
47
  mimeType: block.mimeType
56
48
  }] : []);
57
49
  return many || values.length > 1 ? values : values[0];
58
50
  };
59
- /**
60
- * The encoded success of a tool result lifting the media `lift` finds: the whole success's
61
- * blocks, or its structured content with each media field taken from its blocks alone, an
62
- * empty one when nothing else was left.
63
- */
64
- const reassembled = (lift, result) => {
51
+ const encodedSuccessOf = (lift, result) => {
65
52
  const [whole] = lift.fields;
66
- if (whole !== void 0 && whole.name === void 0) return mediaOf(whole, result);
53
+ if (whole !== void 0 && whole.name === void 0) return mediaFromBlocks(whole, result);
67
54
  const { structuredContent = {} } = result;
68
55
  if (!Predicate.isObject(structuredContent)) return structuredContent;
69
56
  const lifted = new Set(lift.fields.map(({ name }) => name));
70
57
  return Object.fromEntries([...Object.entries(structuredContent).filter(([key]) => !lifted.has(key)), ...lift.fields.flatMap((field) => {
71
- const value = mediaOf(field, result);
58
+ const value = mediaFromBlocks(field, result);
72
59
  return field.name === void 0 || value === void 0 ? [] : [[field.name, value]];
73
60
  })]);
74
61
  };
75
- /** Fail with the value of one of `errors` that `text` holds, or else with `otherwise`. */
76
62
  const failWith = (errors, text, otherwise) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))))(text).pipe(Effect.mapError(() => otherwise), Effect.flatMap(Effect.fail));
77
- /**
78
- * The tool call of `action`, whose success its tool lifts as `lift` says, with `input` on
79
- * `client`, sending to `url`.
80
- */
81
63
  const callTool = (client, url, action, lift, input) => {
82
64
  const { name } = action;
83
65
  const other = (answer) => new McpCallError({ message: `MCP tools/call "${name}" ${answer}` });
@@ -101,7 +83,7 @@ const callTool = (client, url, action, lift, input) => {
101
83
  return yield* failWith(projectedErrors(action), error, other(`returned an error: ${error}`));
102
84
  }
103
85
  if (result.structuredContent === void 0 && lift.rest !== void 0) return yield* Effect.fail(other(`returned no structured content: ${text}`));
104
- return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(reassembled(lift, result));
86
+ return yield* Schema.decodeUnknownEffect(Schema.toCodecJson(action.success))(encodedSuccessOf(lift, result));
105
87
  });
106
88
  };
107
89
  function mcpClient(actions, { url = defaultPath, transformClient = identity } = {}) {
@@ -110,13 +92,14 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
110
92
  const client = transformClient(native);
111
93
  return Object.fromEntries(actions.map((action) => {
112
94
  const lift = liftOf(action.success);
113
- return [action.name, (...args) => Effect.flatMap(inputOf(action, args), (input) => callTool(client, url, action, lift, input))];
95
+ return [action.name, checked(action, (input) => callTool(client, url, action, lift, input))];
114
96
  }));
115
97
  });
116
98
  }
117
99
  /**
118
100
  * One stateless MCP request of `method` with `params`, as `ActionMcp.layerHttp` serves it,
119
- * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers, `mcp-name` from
101
+ * as the native request value: the JSON-RPC envelope, the 2026-07-28 headers (`Accept`,
102
+ * `MCP-Protocol-Version`, `MCP-Method`), `mcp-name` from
120
103
  * `params.uri` for `resources/read` and `params.name` otherwise, and the client metadata in
121
104
  * `_meta` are filled in, under any `_meta` given, such as a `progressToken`; the protocol
122
105
  * version is always the request's own. The test sends it: `HttpClient.execute` answers the
@@ -126,8 +109,8 @@ function mcpClient(actions, { url = defaultPath, transformClient = identity } =
126
109
  * a call its types would not send.
127
110
  */
128
111
  const mcpRequest = (method, params = {}, { headers = {}, url = defaultPath } = {}) => {
129
- const { headers: routing, body } = statelessRequest(method, params);
130
- return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routing), HttpClientRequest.bodyJsonUnsafe(body));
112
+ const { headers: routingHeaders, body } = statelessRequest(method, params);
113
+ return HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.setHeaders(routingHeaders), HttpClientRequest.bodyJsonUnsafe(body));
131
114
  };
132
115
  //#endregion
133
116
  export { McpCallError, layer, mcpClient, mcpRequest };
@@ -1,11 +1,11 @@
1
- import { Anyone } from "./actions.mjs";
2
- import { Refusal } from "./errors.mjs";
3
- import { Protected, ServedRequest, Serving } from "./implementation.mjs";
1
+ import { Anyone } from "../contract/rules.mjs";
2
+ import { Refusal } from "../contract/errors.mjs";
3
+ import { BuildServices, Protected, ServedRequest, Serving } from "../contract/implementation.mjs";
4
4
  import { Action_d_exports, Any as Any$1, Errors } from "../Action.mjs";
5
5
  import { Context, Effect } from "effect";
6
6
  import { HttpRouter, HttpServerResponse } from "effect/http";
7
7
  import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api";
8
- //#region src/internal/authentication.d.ts
8
+ //#region src/authentication/provider.d.ts
9
9
  /** One native security scheme: Bearer unless a descriptor names another. */
10
10
  export type Security = HttpApiSecurity.HttpApiSecurity;
11
11
  /** What a native scheme decodes: a `Redacted` token or key, or Basic credentials. */
@@ -34,6 +34,12 @@ type Response = Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, un
34
34
  interface Runtime$1 {
35
35
  /** The descriptor the provider was built from, the one a binding naming it must hold. */
36
36
  readonly descriptor: unknown;
37
+ /**
38
+ * The verifier itself, for a surface answering refusals its own way, such as RPC: the
39
+ * credential to the identity, or a refusal, or an error the descriptor declares. An empty
40
+ * credential, as the native decoder gives an absent one, is refused without reaching it.
41
+ */
42
+ readonly authenticate: (credential: Credential) => Effect.Effect<unknown, unknown, unknown>;
37
43
  /** The native security middleware's implementation, keyed by its scheme. */
38
44
  readonly middleware: Readonly<Record<string, (route: Response, options: {
39
45
  readonly credential: Credential;
@@ -43,6 +49,11 @@ interface Runtime$1 {
43
49
  * answered with its challenge under Bearer; as it is under another scheme.
44
50
  */
45
51
  readonly stepUp: HttpApiMiddleware.HttpApiMiddleware<never, never, never>;
52
+ /**
53
+ * An MCP endpoint's request: decoded here, and a protected tool's call promotes what it
54
+ * verified. Where `optional`, a request presenting no credential, which the scheme decodes as
55
+ * empty, passes signed out.
56
+ */
46
57
  readonly http: <E, R>(route: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>, optional: boolean) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R | HttpRouter.Provided>;
47
58
  /**
48
59
  * A route of the host's own: the request is decoded here, the route gets the identity
@@ -61,7 +72,7 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
61
72
  readonly security: S;
62
73
  /**
63
74
  * What its verifier may fail with besides a refusal, as a list: every protected endpoint
64
- * declares it, and every remote surface sends it with its status.
75
+ * declares it, and every remote surface sends it, over HTTP with its status.
65
76
  */
66
77
  readonly error: E;
67
78
  readonly "~provider": Context.Key<Provider<I, Name>, Runtime$1>;
@@ -69,12 +80,12 @@ export interface Descriptor<I, A, S extends Security, Name extends string = stri
69
80
  /** An MCP request's verified identity, which a protected tool's call promotes. */
70
81
  readonly "~verified": Context.Key<never, unknown>;
71
82
  }
72
- export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
73
- export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
74
83
  /**
75
- * The identity a descriptor for `A` must authenticate: `unknown` for an erased action, whose
76
- * key is known only at run time, where `assertAuthentication` checks it.
84
+ * Any descriptor `make` returns, named so a package emitting declarations can export one, and a
85
+ * binding name one.
77
86
  */
87
+ export type Any = Descriptor<unknown, unknown, Security, string, Errors>;
88
+ export type Identity<A extends Any$1> = [A] extends [never] ? never : A["caller"] extends Context.Key<infer I, unknown> ? I : never;
78
89
  type Covered<A extends Any$1> = Exclude<A["caller"], typeof Anyone> extends Context.Key<infer I, unknown> ? I : never;
79
90
  export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
80
91
  readonly authentication?: Any;
@@ -82,6 +93,16 @@ export type Required<A extends Any$1> = [Protected<A>] extends [never] ? {
82
93
  readonly authentication: Descriptor<Covered<Protected<A>>, unknown, Security, string, Errors>;
83
94
  };
84
95
  export type ProviderOf<D> = D extends Descriptor<infer I, unknown, Security, infer Name, Errors> ? Provider<I, Name> : never;
96
+ /** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
97
+ export type PublicOnly<Actions extends ReadonlyArray<Any$1>> = [Exclude<Actions[number], {
98
+ readonly caller: typeof Anyone;
99
+ }>] extends [never] ? unknown : {
100
+ readonly "Protected actions take options naming their authentication": never;
101
+ };
102
+ /** The descriptor options `O` name, where they always name one. */
103
+ export type DescriptorOf<O> = [O] extends [{
104
+ readonly authentication: infer D extends Any;
105
+ }] ? D : undefined;
85
106
  /** All protected actions on a surface use the descriptor's single identity key. */
86
107
  export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>> ? unknown : [Exclude<Covered<Protected<A>>, D extends Descriptor<infer I, unknown, Security, string, Errors> ? I : never>] extends [never] ? unknown : {
87
108
  readonly "Authentication descriptor does not cover every protected action": never;
@@ -90,6 +111,17 @@ export type Matching<A extends Any$1, D> = unknown extends Covered<Protected<A>>
90
111
  export type RemoteRequest<App, A extends Any$1> = ServedRequest<App, Extract<A, {
91
112
  readonly caller: typeof Anyone;
92
113
  }>> | Exclude<ServedRequest<App, Protected<A>>, Identity<Protected<A>>>;
114
+ /**
115
+ * What a server of the actions `A` of the implementations `App` reads at startup, as
116
+ * `BuildServices` states it, with the caller a builder yields named as the mistake it is: a server
117
+ * has its caller per request.
118
+ */
119
+ export type ServerBuildServices<App, A extends Any$1> = App extends unknown ? BuildServices<App, A> extends (infer R) ? R extends Identity<Protected<A>> ? {
120
+ readonly "A builder yields the caller, which a server has per request: yield it in a handler or an authorizer": {
121
+ readonly caller: R;
122
+ readonly builderOf: Serving<App, A>["name"];
123
+ };
124
+ } : R : never : never;
93
125
  /**
94
126
  * What a call of `A` under the descriptor `D` may fail with from its verifier: the errors `D`
95
127
  * declares, for a protected action; none for a public one, which nothing verifies.
@@ -0,0 +1,12 @@
1
+ import { Unauthenticated } from "../contract/errors.mjs";
2
+ import { Anyone } from "../contract/rules.mjs";
3
+ import { Effect, Option } from "effect";
4
+ //#region src/authentication/provider.ts
5
+ const withVerifierErrors = (action, errors, verifierErrors) => action.caller === Anyone || verifierErrors === void 0 ? errors : [...errors, ...verifierErrors];
6
+ const assertAuthentication = (actions, auth) => {
7
+ for (const action of actions) if (action.caller !== Anyone && (auth === void 0 || action.caller.key !== auth.identity.key)) throw new Error(`Protected action '${action.name}' requires its matching authentication descriptor`);
8
+ };
9
+ const providerOf = (auth) => Effect.flatMap(Effect.service(auth["~provider"]), (provider) => provider.descriptor === auth ? Effect.succeed(provider) : Effect.die(/* @__PURE__ */ new Error(`Authentication "${auth.name}": the binding's descriptor is not its provider's; build both from one descriptor`)));
10
+ const promote = (auth, run) => Effect.flatMap(Effect.serviceOption(auth["~verified"]), (actor) => Option.isSome(actor) ? Effect.provideService(run, auth.identity, actor.value) : Effect.fail(new Unauthenticated()));
11
+ //#endregion
12
+ export { assertAuthentication, promote, providerOf, withVerifierErrors };
@@ -0,0 +1,36 @@
1
+ import { Refusal, statuses } from "../contract/errors.mjs";
2
+ import { Cause, Context, Effect, Exit, Option, Predicate, Ref, Schema } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+ //#region src/authentication/refusal.ts
5
+ const rfc6750ErrorDescriptionCharacters = /^[\x20\x21\x23-\x5B\x5D-\x7E]+$/;
6
+ const challenge = (scheme, parameters) => {
7
+ const given = parameters.flatMap(([name, value]) => value === void 0 ? [] : [`${name}="${value.replace(/["\\]/g, "\\$&")}"`]);
8
+ return given.length === 0 ? scheme : `${scheme} ${given.join(", ")}`;
9
+ };
10
+ const bearer = (parameters) => challenge("Bearer", parameters);
11
+ const encode = Schema.encodeSync(Refusal);
12
+ const isRefusal = Schema.is(Refusal);
13
+ const insufficientScopeChallenge = (error, metadataUrl) => Predicate.isTagged(error, "Forbidden") && error.scopes !== void 0 ? bearer([
14
+ ["error", "insufficient_scope"],
15
+ ["scope", error.scopes.join(" ")],
16
+ ["resource_metadata", metadataUrl],
17
+ ["error_description", rfc6750ErrorDescriptionCharacters.test(error.message) ? error.message : void 0]
18
+ ]) : void 0;
19
+ const plain = (error) => HttpServerResponse.jsonUnsafe(encode(error), { status: statuses[error._tag] });
20
+ const answer = (error, metadataUrl) => {
21
+ const response = plain(error);
22
+ const scoped = insufficientScopeChallenge(error, metadataUrl);
23
+ return scoped === void 0 ? response : HttpServerResponse.setHeader(response, "www-authenticate", scoped);
24
+ };
25
+ var SteppedUpRefusal = class extends Context.Service()("effect-actions/SteppedUp") {};
26
+ const isStepUp = (error) => isRefusal(error) && (!Predicate.isTagged(error, "Forbidden") || error.scopes !== void 0);
27
+ const recordStepUp = (call) => Effect.tapError(call, (error) => isStepUp(error) ? Effect.flatMap(Effect.serviceOption(SteppedUpRefusal), (slot) => Option.isSome(slot) ? Ref.set(slot.value, Option.some(error)) : Effect.void) : Effect.void);
28
+ const answerStepUp = (route, metadataUrl) => Effect.gen(function* () {
29
+ const slot = yield* Ref.make(Option.none());
30
+ const exit = yield* Effect.exit(Effect.provideService(route, SteppedUpRefusal, slot));
31
+ const refused = yield* Ref.get(slot);
32
+ const diedOrInterruptedAfterRefusal = Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause));
33
+ return Option.isSome(refused) && !diedOrInterruptedAfterRefusal ? answer(refused.value, metadataUrl) : yield* exit;
34
+ });
35
+ //#endregion
36
+ export { answer, answerStepUp, bearer, challenge, isRefusal, isStepUp, plain, recordStepUp };
@@ -1,14 +1,11 @@
1
- import "./errors.mjs";
1
+ import "../contract/errors.mjs";
2
2
  import { Action_d_exports, Any } from "../Action.mjs";
3
3
  import { Effect, Runtime, Types } from "effect";
4
4
  import { CliError, Command } from "effect/cli";
5
- //#region src/internal/cli.d.ts
6
- /**
7
- * The fields of `A`'s input that may be positional: the named top-level fields of a struct
8
- * or class input, as encoded. None for any other input, a record or a union included.
9
- */
10
- export type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
11
- /** How one command is named, takes its input and prints its result. */
5
+ //#region src/cli/commands.d.ts
6
+ type Field<A extends Any> = A["input"]["Encoded"] extends (infer E) ? true extends Types.IsUnion<E> ? never : E extends ReadonlyArray<unknown> ? never : E extends object ? string extends keyof E ? never : Extract<keyof E, string> : never : never;
7
+ type StdinField<A extends Any> = A["input"]["Encoded"] extends (infer E) ? Exclude<Field<A>, { [K in keyof E]-?: {} extends Pick<E, K> ? K : never; }[keyof E]> : never;
8
+ /** How one command of `A` is named, takes its input and prints its result. */
12
9
  export interface Options<A extends Any> {
13
10
  /** Override the command name. The action name, in kebab case, is used by default. */
14
11
  readonly name?: string;
@@ -24,12 +21,19 @@ export interface Options<A extends Any> {
24
21
  * `Param.withAlias` takes it.
25
22
  */
26
23
  readonly aliases?: { readonly [K in Field<A>]?: string; };
24
+ /**
25
+ * A required field read from stdin instead of a flag, such as a secret, which a flag would
26
+ * leave in the process list and the shell's history.
27
+ */
28
+ readonly stdin?: StdinField<A>;
27
29
  }
28
30
  /**
29
- * What a command fails with when its action fails: Effect CLI's own error for a handler's
30
- * failure, which `Command.run` prints on stderr and marks reported, so `runMain` does not
31
- * print it again. Its `cause` is the action's failure. The process exits with the cause's
32
- * `Runtime.errorExitCode`, 1 by default.
31
+ * What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
32
+ * action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
33
+ * prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
34
+ * the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
35
+ * reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
36
+ * fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
33
37
  */
34
38
  export declare class UserError<out E = unknown> extends CliError.UserError {
35
39
  readonly cause: E;
@@ -0,0 +1,302 @@
1
+ import { InvalidInput } from "../contract/errors.mjs";
2
+ import { assertDistinct, literalValues, members, projectedErrors, unsuspended } from "../contract/rules.mjs";
3
+ import { withStderrConsole } from "../stdio/console.mjs";
4
+ import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST, Stdio, Stream, flow } from "effect";
5
+ import { CliError, Command, Flag, Param } from "effect/cli";
6
+ //#region src/cli/commands.ts
7
+ const kebab = (name) => {
8
+ const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
9
+ return kebabbed === "" ? name : kebabbed;
10
+ };
11
+ const renderedCommandJsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
12
+ const jsonKinds = [
13
+ "null",
14
+ "boolean",
15
+ "number",
16
+ "string",
17
+ "array",
18
+ "object"
19
+ ];
20
+ const jsonKindOf = (value) => value === null ? "null" : Predicate.isBoolean(value) ? "boolean" : Predicate.isNumber(value) ? "number" : Predicate.isString(value) ? "string" : Array.isArray(value) ? "array" : "object";
21
+ const acceptedJsonKindsOf = (ast) => members(ast).flatMap((member) => {
22
+ if (SchemaAST.isLiteral(member)) return [jsonKindOf(member.literal)];
23
+ if (SchemaAST.isEnum(member)) return member.enums.map(([, value]) => jsonKindOf(value));
24
+ if (SchemaAST.isNull(member)) return ["null"];
25
+ if (SchemaAST.isBoolean(member)) return ["boolean"];
26
+ if (SchemaAST.isNumber(member)) return ["number"];
27
+ if (SchemaAST.isString(member) || SchemaAST.isTemplateLiteral(member)) return ["string"];
28
+ if (SchemaAST.isArrays(member)) return ["array"];
29
+ if (SchemaAST.isObjects(member)) return ["object"];
30
+ return jsonKinds;
31
+ });
32
+ const jsonOfAcceptedKindOrText = (encoded) => {
33
+ const accepted = new Set(acceptedJsonKindsOf(encoded));
34
+ return Schema.Union([Schema.fromJsonString(Schema.Json).check(Schema.makeFilter((json) => accepted.has(jsonKindOf(json)))), Schema.String]);
35
+ };
36
+ const stringChoicesOf = (ast) => {
37
+ const accepted = members(ast).flatMap(literalValues);
38
+ return accepted.every(Predicate.isString) ? accepted : void 0;
39
+ };
40
+ const takesTextAsIs = (encoded) => stringChoicesOf(encoded) !== void 0 || SchemaAST.isString(encoded) || SchemaAST.isTemplateLiteral(encoded);
41
+ const valueParam = (kind, name, field) => {
42
+ const encoded = unsuspended(field);
43
+ if (takesTextAsIs(encoded)) {
44
+ const literals = stringChoicesOf(encoded);
45
+ return literals === void 0 ? Param.String(kind, name) : Param.Literals(kind, name, literals);
46
+ }
47
+ if (SchemaAST.isBoolean(encoded)) return Param.Boolean(kind, name);
48
+ return Param.String(kind, name).pipe(Param.withSchema(jsonOfAcceptedKindOrText(encoded)), Param.withMetavar("value"));
49
+ };
50
+ const withoutOptionalAbsence = (ast) => {
51
+ const members = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
52
+ return members.length === 1 && members[0] !== void 0 ? members[0] : ast;
53
+ };
54
+ const optionalAddedNull = (plain) => {
55
+ const types = members(plain);
56
+ return types.some(SchemaAST.isUndefined) && !types.some(SchemaAST.isNull);
57
+ };
58
+ const repeatedElementOf = (ast) => {
59
+ const encoded = unsuspended(ast);
60
+ if (!SchemaAST.isArrays(encoded)) return void 0;
61
+ const [element, ...after] = encoded.rest;
62
+ if (element === void 0 || after.length > 0 || !encoded.elements.every((first) => first === element) || !acceptedJsonKindsOf(element).every((kind) => kind === "string" || kind === "number")) return;
63
+ return {
64
+ element,
65
+ min: encoded.elements.length
66
+ };
67
+ };
68
+ const noElementOccurrence = "[]";
69
+ const jsonHintOf = (encoded) => {
70
+ const value = unsuspended(encoded);
71
+ const kinds = new Set(acceptedJsonKindsOf(value));
72
+ if (kinds.size !== 1) return void 0;
73
+ if (kinds.has("object")) return "A JSON object.";
74
+ if (!kinds.has("array")) return void 0;
75
+ const fields = elementFieldsOf(value);
76
+ return fields === void 0 ? "A JSON array." : `A JSON array of {${fields.join(", ")}}.`;
77
+ };
78
+ const elementFieldsOf = (array) => {
79
+ if (!SchemaAST.isArrays(array)) return void 0;
80
+ const [element, ...others] = [...array.elements, ...array.rest];
81
+ if (element === void 0 || others.some((other) => other !== element)) return void 0;
82
+ const fields = namedFieldsOf(unsuspended(element));
83
+ return fields.size === 0 ? void 0 : Array.from(fields, ([name, field]) => SchemaAST.isOptional(field) ? `${String(name)}?` : String(name));
84
+ };
85
+ const withHint = (description, hint) => {
86
+ if (hint === void 0 || description === void 0) return description ?? hint;
87
+ const stated = description.trimEnd();
88
+ return /[.!?:]$/.test(stated) ? `${stated} ${hint}` : `${stated}. ${hint}`;
89
+ };
90
+ const fieldParam = (kind, name, encoded, plain) => {
91
+ const optional = SchemaAST.isOptional(encoded);
92
+ const value = optional && plain !== void 0 && optionalAddedNull(plain) ? withoutOptionalAbsence(encoded) : encoded;
93
+ const repeats = repeatedElementOf(value);
94
+ if (repeats !== void 0) {
95
+ const literals = stringChoicesOf(unsuspended(repeats.element));
96
+ const elements = literals === void 0 ? valueParam(kind, name, repeats.element) : Param.Literals(kind, name, [...literals, noElementOccurrence]);
97
+ const listed = (values) => Option.some(values.filter((value) => value !== noElementOccurrence));
98
+ return {
99
+ param: optional ? Param.variadic(elements).pipe(Param.map((values) => values.length === 0 ? Option.none() : listed(values))) : Param.variadic(elements, { min: repeats.min }).pipe(Param.map(listed)),
100
+ repeats: true,
101
+ hint: kind === Param.flagKind ? "Repeat for each value." : void 0
102
+ };
103
+ }
104
+ if (optional) return {
105
+ param: Param.optional(valueParam(kind, name, value)),
106
+ repeats: false,
107
+ hint: kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(value)) ? `--no-${name} sends false.` : jsonHintOf(value)
108
+ };
109
+ const isSwitch = kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(encoded));
110
+ return {
111
+ param: isSwitch ? Flag.Boolean(name).pipe(Flag.withDefault(false), Flag.map(Option.some)) : valueParam(kind, name, encoded).pipe(Param.map(Option.some)),
112
+ repeats: false,
113
+ hint: isSwitch ? "Left out, it sends false." : jsonHintOf(encoded)
114
+ };
115
+ };
116
+ const namedFieldsOf = (ast) => {
117
+ const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
118
+ return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
119
+ };
120
+ const isTransformedAsWhole = (ast) => (SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast)?.encoding !== void 0;
121
+ const ownOrSuspendedDescriptionOf = (ast) => ast.context?.annotations?.description ?? SchemaAST.resolveDescription(ast) ?? describedBeforeLastCheck(ast) ?? (SchemaAST.isSuspend(ast) ? ownOrSuspendedDescriptionOf(ast.thunk()) : void 0);
122
+ const describedBeforeLastCheck = (ast) => [ast.annotations, ...(ast.checks ?? []).map((check) => check.annotations)].map((annotations) => annotations?.["description"]).findLast(Predicate.isString);
123
+ const describedThroughAbsence = (ast) => ownOrSuspendedDescriptionOf(ast) ?? ownOrSuspendedDescriptionOf(withoutOptionalAbsence(ast));
124
+ const describedByElement = (ast) => {
125
+ const array = unsuspended(withoutOptionalAbsence(ast));
126
+ const element = SchemaAST.isArrays(array) ? array.rest[0] : void 0;
127
+ return element === void 0 ? void 0 : describedThroughAbsence(element);
128
+ };
129
+ const fieldParams = (encoded, input, positional, aliases) => {
130
+ const plainEncodedFields = namedFieldsOf(SchemaAST.toEncoded(input));
131
+ const declaredFieldsUnlessRenamed = namedFieldsOf(isTransformedAsWhole(input) ? encoded : SchemaAST.toType(input));
132
+ return encoded.propertySignatures.map((property) => {
133
+ const field = String(property.name);
134
+ const declaredField = declaredFieldsUnlessRenamed.get(property.name);
135
+ const kind = positional.includes(field) ? Param.argumentKind : Param.flagKind;
136
+ const { param, repeats, hint } = fieldParam(kind, kebab(field), property.type, plainEncodedFields.get(property.name));
137
+ const alias = kind === Param.flagKind ? aliases[field] : void 0;
138
+ const aliased = alias === void 0 ? param : Param.withAlias(param, alias);
139
+ const documented = declaredField ?? property.type;
140
+ const description = describedThroughAbsence(documented) ?? describedThroughAbsence(property.type) ?? (repeats ? describedByElement(documented) ?? describedByElement(property.type) : void 0);
141
+ const help = withHint(description, hint);
142
+ return {
143
+ field,
144
+ param: help === void 0 ? aliased : Param.withDescription(aliased, help),
145
+ optional: SchemaAST.isOptional(property.type),
146
+ repeats,
147
+ hint,
148
+ description
149
+ };
150
+ });
151
+ };
152
+ const positionalOrder = (params, positional) => {
153
+ assertDistinct("positional argument", positional, (field) => field);
154
+ const ordered = positional.map((field) => {
155
+ const found = params.find((param) => param.field === field);
156
+ if (found === void 0) throw new Error(`Not an input field: ${field}`);
157
+ return found;
158
+ });
159
+ const firstOptional = ordered.findIndex((param) => param.optional);
160
+ const misplaced = ordered.slice(firstOptional === -1 ? ordered.length : firstOptional).find((param) => !param.optional);
161
+ if (misplaced !== void 0) throw new Error(`Required positional argument after an optional one: ${misplaced.field}`);
162
+ const repeatedBeforeLast = ordered.slice(0, -1).find((param) => param.repeats);
163
+ if (repeatedBeforeLast !== void 0) throw new Error(`Repeated positional argument before another one: ${repeatedBeforeLast.field}`);
164
+ return ordered;
165
+ };
166
+ const wholeInputFlag = (encoded) => Flag.String("input").pipe(Flag.withSchema(jsonOfAcceptedKindOrText(encoded)), Flag.withDescription("Whole action input as JSON; omitted, {}"), Flag.optional);
167
+ const printedOutput = (action, value, render) => Effect.map(Effect.orDie(Schema.encodeEffect(Schema.toCodecJson(action.success))(value)), (encoded) => render === void 0 && SchemaAST.isVoid(action.success.ast) ? Option.none() : Option.some(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value)));
168
+ /**
169
+ * What a command fails with when its action fails: Effect CLI's `UserError`, typed with the
170
+ * action's failure as its `cause` and `reason`, and whose message is its JSON, which `Command.run`
171
+ * prints on stderr and marks reported, so `runMain` does not print it again. The process exits with
172
+ * the cause's `Runtime.errorExitCode`, 1 by default. A type only: after `Command.run`, match the
173
+ * reason by its tag, `Effect.catchReason("UserError", "UserNotFound", f)`. A command of your own
174
+ * fails with one through `toUserError`; any other failure is reported as a crash, `ERROR (#n)`.
175
+ */
176
+ var UserError = class extends CliError.UserError {
177
+ /**
178
+ * The action's failure, as Effect names an error's nested one, so the native
179
+ * `Effect.catchReason("UserError", "UserNotFound", f)` and `catchReasons` match it by tag.
180
+ */
181
+ get reason() {
182
+ return this.cause;
183
+ }
184
+ get [Runtime.errorExitCode]() {
185
+ return Runtime.getErrorExitCode(this.cause);
186
+ }
187
+ };
188
+ const ownStringProperty = (error, key) => {
189
+ if (!Predicate.hasProperty(error, key)) return void 0;
190
+ const value = error[key];
191
+ return Predicate.isString(value) ? value : void 0;
192
+ };
193
+ const describedWithoutFields = (error, seen = /* @__PURE__ */ new Set()) => {
194
+ const name = ownStringProperty(error, "_tag") ?? (error instanceof Error ? error.name : "Error");
195
+ const message = Predicate.isString(error) ? error : ownStringProperty(error, "message") ?? "";
196
+ const named = message === "" ? name : `${name}: ${message}`;
197
+ seen.add(error);
198
+ return Predicate.hasProperty(error, "cause") && error.cause !== void 0 && !seen.has(error.cause) ? `${named}: ${describedWithoutFields(error.cause, seen)}` : named;
199
+ };
200
+ const userErrorOf = (errors) => {
201
+ const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(errors))));
202
+ return (error) => encode(error, { onExcessProperty: "error" }).pipe(Effect.catch(() => encode(error)), Effect.orElseSucceed(() => describedWithoutFields(error)), Effect.flatMap((userMessage) => Effect.fail(new UserError({
203
+ cause: error,
204
+ userMessage
205
+ }))));
206
+ };
207
+ const failingAsDerived = (actions, surfaceErrorsOf) => {
208
+ const failure = userErrorOf([...new Set(actions.flatMap((action) => projectedErrors(action, surfaceErrorsOf(action))))]);
209
+ return (self) => Effect.catchIf(self, (error) => !CliError.isCliError(error), failure);
210
+ };
211
+ const unreadableStdinField = (field, message) => new InvalidInput({
212
+ message,
213
+ issues: [{
214
+ path: [field],
215
+ message
216
+ }]
217
+ });
218
+ const fieldValueFromText = (field) => {
219
+ const encoded = unsuspended(field);
220
+ return takesTextAsIs(encoded) ? (text) => text : Schema.decodeSync(jsonOfAcceptedKindOrText(encoded));
221
+ };
222
+ const stdinTextOf = (field) => Effect.gen(function* () {
223
+ const stdio = yield* Effect.serviceOption(Stdio.Stdio);
224
+ if (Option.isNone(stdio)) return yield* Effect.die(/* @__PURE__ */ new Error("Stdio is not provided"));
225
+ if (yield* stdio.value.stdinIsTerminal) return yield* unreadableStdinField(field, `${field} is read from stdin, which is a terminal: pipe it in`);
226
+ return (yield* stdio.value.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError((error) => unreadableStdinField(field, `stdin cannot be read: ${error.message}`)))).replace(/[\r\n]+$/, "");
227
+ });
228
+ const inputConfig = (action, positional, aliases, stdin) => {
229
+ const codec = Schema.toCodecJson(action.input);
230
+ const encoded = unsuspended(SchemaAST.toEncoded(codec.ast));
231
+ const decodeRefusingUndeclared = Schema.decodeUnknownEffect(codec, { onExcessProperty: "error" });
232
+ const decode = flow(decodeRefusingUndeclared, Effect.mapError(InvalidInput.fromSchemaError));
233
+ if (SchemaAST.isObjects(encoded) && encoded.indexSignatures.every((signature) => SchemaAST.isNever(signature.type))) {
234
+ const params = fieldParams(encoded, unsuspended(action.input.ast), positional, aliases);
235
+ const fromStdin = Option.map(Option.fromUndefinedOr(stdin), (field) => {
236
+ const property = encoded.propertySignatures.find(({ name }) => name === field);
237
+ if (property === void 0 || SchemaAST.isOptional(property.type)) throw new Error(`Not a required input field: ${field}`);
238
+ if (positional.includes(field)) throw new Error(`Both positional and stdin: ${field}`);
239
+ const description = params.find((param) => param.field === field)?.description;
240
+ return {
241
+ field,
242
+ value: fieldValueFromText(property.type),
243
+ help: `Reads ${kebab(field)} from stdin${description === void 0 ? "." : `: ${description}`}`
244
+ };
245
+ });
246
+ const flagged = new Set(params.map(({ field }) => field).filter((field) => !positional.includes(field) && field !== stdin));
247
+ const stray = Object.keys(aliases).find((field) => !flagged.has(field));
248
+ if (stray !== void 0) throw new Error(`Not a flag's input field: ${stray}`);
249
+ return {
250
+ flags: Object.fromEntries(params.flatMap(({ field, param }) => flagged.has(field) ? [[field, param]] : [])),
251
+ positional: positionalOrder(params, positional),
252
+ decode: (parsed) => Option.match(fromStdin, {
253
+ onNone: () => decode(Record.getSomes(parsed)),
254
+ onSome: ({ field, value }) => Effect.flatMap(stdinTextOf(field), (text) => decode({
255
+ ...Record.getSomes(parsed),
256
+ [field]: value(text)
257
+ }))
258
+ }),
259
+ stdinHelp: Option.getOrUndefined(Option.map(fromStdin, ({ help }) => help))
260
+ };
261
+ }
262
+ if (positional.length > 0) throw new Error(`Positional arguments need named input fields: ${positional.join(", ")}`);
263
+ if (Object.keys(aliases).length > 0) throw new Error(`Aliases need named input fields: ${Object.keys(aliases).join(", ")}`);
264
+ if (stdin !== void 0) throw new Error(`Stdin needs named input fields: ${stdin}`);
265
+ return {
266
+ flags: { input: wholeInputFlag(encoded) },
267
+ positional: [],
268
+ decode: (parsed) => decode(Option.getOrElse(parsed["input"] ?? Option.none(), () => ({}))),
269
+ stdinHelp: void 0
270
+ };
271
+ };
272
+ const command = (action, execute, options, errors = []) => {
273
+ const name = options?.name ?? kebab(action.name);
274
+ const render = options?.render;
275
+ const { flags, positional, decode, stdinHelp } = inputConfig(action, options?.positional ?? [], options?.aliases ?? {}, options?.stdin);
276
+ const failure = userErrorOf(projectedErrors(action, errors));
277
+ assertDistinct("flag", [
278
+ ...Object.keys(flags).map((field) => [`--${kebab(field)}`, `field ${field}`]),
279
+ ...render === void 0 ? [] : [["--json", "render's --json"]],
280
+ ...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => Predicate.isString(alias) ? [[`--${alias.replace(/^-+/, "")}`, `alias of field ${field}`]] : [])
281
+ ], ([flag]) => flag, ([, claimant]) => claimant);
282
+ const args = positional.map(({ param }) => param);
283
+ const parsedFields = (input, values) => ({
284
+ ...input,
285
+ ...Object.fromEntries(positional.map(({ field }, index) => [field, values[index] ?? Option.none()]))
286
+ });
287
+ const run = (parsed, rendered) => decode(parsed).pipe(Effect.flatMap(execute), Effect.catch(failure), Effect.flatMap((value) => printedOutput(action, value, rendered)), withStderrConsole, Effect.flatMap(Option.match({
288
+ onNone: () => Effect.void,
289
+ onSome: Console.log
290
+ })));
291
+ const command = render === void 0 ? Command.make(name, {
292
+ input: flags,
293
+ args
294
+ }, ({ input, args: values }) => run(parsedFields(input, values), void 0)) : Command.make(name, {
295
+ input: flags,
296
+ args,
297
+ json: renderedCommandJsonFlag
298
+ }, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render));
299
+ return stdinHelp === void 0 ? Command.withDescription(command, action.description) : command.pipe(Command.withDescription(`${action.description}\n\n${stdinHelp}`), Command.withShortDescription(action.description));
300
+ };
301
+ //#endregion
302
+ export { UserError, command, failingAsDerived, kebab };