effect 4.0.0-beta.25 → 4.0.0-beta.26

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 (110) hide show
  1. package/dist/Config.js +4 -4
  2. package/dist/Config.js.map +1 -1
  3. package/dist/Effect.d.ts +20 -13
  4. package/dist/Effect.d.ts.map +1 -1
  5. package/dist/Effect.js +2 -1
  6. package/dist/Effect.js.map +1 -1
  7. package/dist/Runtime.d.ts +66 -0
  8. package/dist/Runtime.d.ts.map +1 -1
  9. package/dist/Runtime.js +72 -5
  10. package/dist/Runtime.js.map +1 -1
  11. package/dist/Schema.d.ts +15 -32
  12. package/dist/Schema.d.ts.map +1 -1
  13. package/dist/Schema.js +9 -17
  14. package/dist/Schema.js.map +1 -1
  15. package/dist/SchemaAST.js +91 -2
  16. package/dist/SchemaAST.js.map +1 -1
  17. package/dist/SchemaGetter.d.ts +3 -3
  18. package/dist/SchemaRepresentation.d.ts.map +1 -1
  19. package/dist/SchemaRepresentation.js +12 -8
  20. package/dist/SchemaRepresentation.js.map +1 -1
  21. package/dist/Stdio.d.ts +10 -2
  22. package/dist/Stdio.d.ts.map +1 -1
  23. package/dist/Stdio.js +18 -0
  24. package/dist/Stdio.js.map +1 -1
  25. package/dist/internal/schema/representation.js +7 -3
  26. package/dist/internal/schema/representation.js.map +1 -1
  27. package/dist/internal/schema/to-codec.js +7 -10
  28. package/dist/internal/schema/to-codec.js.map +1 -1
  29. package/dist/unstable/ai/AiError.d.ts +6 -3
  30. package/dist/unstable/ai/AiError.d.ts.map +1 -1
  31. package/dist/unstable/ai/AiError.js +8 -4
  32. package/dist/unstable/ai/AiError.js.map +1 -1
  33. package/dist/unstable/ai/LanguageModel.js +7 -5
  34. package/dist/unstable/ai/LanguageModel.js.map +1 -1
  35. package/dist/unstable/cli/CliError.d.ts +19 -52
  36. package/dist/unstable/cli/CliError.d.ts.map +1 -1
  37. package/dist/unstable/cli/CliError.js +21 -53
  38. package/dist/unstable/cli/CliError.js.map +1 -1
  39. package/dist/unstable/cli/Command.d.ts +117 -62
  40. package/dist/unstable/cli/Command.d.ts.map +1 -1
  41. package/dist/unstable/cli/Command.js +95 -32
  42. package/dist/unstable/cli/Command.js.map +1 -1
  43. package/dist/unstable/cli/GlobalFlag.d.ts +1 -1
  44. package/dist/unstable/cli/GlobalFlag.d.ts.map +1 -1
  45. package/dist/unstable/cli/internal/command.d.ts +27 -10
  46. package/dist/unstable/cli/internal/command.d.ts.map +1 -1
  47. package/dist/unstable/cli/internal/command.js +40 -23
  48. package/dist/unstable/cli/internal/command.js.map +1 -1
  49. package/dist/unstable/cli/internal/config.js +42 -0
  50. package/dist/unstable/cli/internal/config.js.map +1 -1
  51. package/dist/unstable/cli/internal/help.d.ts +4 -4
  52. package/dist/unstable/cli/internal/help.d.ts.map +1 -1
  53. package/dist/unstable/cli/internal/help.js +25 -7
  54. package/dist/unstable/cli/internal/help.js.map +1 -1
  55. package/dist/unstable/cli/internal/parser.js +26 -6
  56. package/dist/unstable/cli/internal/parser.js.map +1 -1
  57. package/dist/unstable/httpapi/HttpApiBuilder.d.ts.map +1 -1
  58. package/dist/unstable/httpapi/HttpApiBuilder.js +1 -7
  59. package/dist/unstable/httpapi/HttpApiBuilder.js.map +1 -1
  60. package/dist/unstable/httpapi/HttpApiClient.d.ts +6 -6
  61. package/dist/unstable/httpapi/HttpApiClient.d.ts.map +1 -1
  62. package/dist/unstable/httpapi/HttpApiClient.js.map +1 -1
  63. package/dist/unstable/httpapi/HttpApiEndpoint.d.ts +10 -10
  64. package/dist/unstable/httpapi/HttpApiEndpoint.d.ts.map +1 -1
  65. package/dist/unstable/httpapi/HttpApiEndpoint.js +2 -2
  66. package/dist/unstable/httpapi/HttpApiEndpoint.js.map +1 -1
  67. package/dist/unstable/httpapi/HttpApiError.d.ts +6 -15
  68. package/dist/unstable/httpapi/HttpApiError.d.ts.map +1 -1
  69. package/dist/unstable/httpapi/HttpApiError.js +16 -21
  70. package/dist/unstable/httpapi/HttpApiError.js.map +1 -1
  71. package/dist/unstable/reactivity/Atom.d.ts +56 -0
  72. package/dist/unstable/reactivity/Atom.d.ts.map +1 -1
  73. package/dist/unstable/reactivity/Atom.js +66 -0
  74. package/dist/unstable/reactivity/Atom.js.map +1 -1
  75. package/dist/unstable/reactivity/AtomHttpApi.d.ts +2 -2
  76. package/dist/unstable/reactivity/AtomHttpApi.d.ts.map +1 -1
  77. package/dist/unstable/rpc/RpcServer.d.ts +3 -0
  78. package/dist/unstable/rpc/RpcServer.d.ts.map +1 -1
  79. package/dist/unstable/rpc/RpcServer.js.map +1 -1
  80. package/dist/unstable/sql/SqlSchema.d.ts +2 -2
  81. package/dist/unstable/sql/SqlSchema.d.ts.map +1 -1
  82. package/dist/unstable/sql/SqlSchema.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/Config.ts +4 -4
  85. package/src/Effect.ts +29 -9
  86. package/src/Runtime.ts +95 -5
  87. package/src/Schema.ts +20 -52
  88. package/src/SchemaAST.ts +127 -2
  89. package/src/SchemaGetter.ts +3 -3
  90. package/src/SchemaRepresentation.ts +12 -8
  91. package/src/Stdio.ts +21 -2
  92. package/src/internal/schema/representation.ts +8 -4
  93. package/src/internal/schema/to-codec.ts +7 -17
  94. package/src/unstable/ai/AiError.ts +8 -4
  95. package/src/unstable/ai/LanguageModel.ts +8 -5
  96. package/src/unstable/cli/CliError.ts +43 -55
  97. package/src/unstable/cli/Command.ts +348 -180
  98. package/src/unstable/cli/GlobalFlag.ts +1 -1
  99. package/src/unstable/cli/internal/command.ts +62 -32
  100. package/src/unstable/cli/internal/config.ts +49 -0
  101. package/src/unstable/cli/internal/help.ts +41 -16
  102. package/src/unstable/cli/internal/parser.ts +36 -10
  103. package/src/unstable/httpapi/HttpApiBuilder.ts +1 -7
  104. package/src/unstable/httpapi/HttpApiClient.ts +6 -6
  105. package/src/unstable/httpapi/HttpApiEndpoint.ts +4 -4
  106. package/src/unstable/httpapi/HttpApiError.ts +23 -21
  107. package/src/unstable/reactivity/Atom.ts +132 -0
  108. package/src/unstable/reactivity/AtomHttpApi.ts +2 -2
  109. package/src/unstable/rpc/RpcServer.ts +5 -0
  110. package/src/unstable/sql/SqlSchema.ts +2 -2
@@ -22,7 +22,7 @@ import * as Flag from "./Flag.ts"
22
22
  * @category models
23
23
  */
24
24
  export interface HandlerContext {
25
- readonly command: Command.Command<any, unknown, unknown, unknown>
25
+ readonly command: Command.Command<any, unknown, any, unknown, unknown>
26
26
  readonly commandPath: ReadonlyArray<string>
27
27
  readonly version: string
28
28
  }
@@ -16,7 +16,7 @@ import type * as GlobalFlag from "../GlobalFlag.ts"
16
16
  import type { ArgDoc, ExampleDoc, FlagDoc, HelpDoc, SubcommandGroupDoc } from "../HelpDoc.ts"
17
17
  import * as Param from "../Param.ts"
18
18
  import * as Primitive from "../Primitive.ts"
19
- import { type ConfigInternal, reconstructTree } from "./config.ts"
19
+ import { type ConfigInternal, emptyConfig, reconstructTree } from "./config.ts"
20
20
 
21
21
  /* ========================================================================== */
22
22
  /* Types */
@@ -26,19 +26,23 @@ import type { Command, CommandContext, Environment, ParsedTokens } from "../Comm
26
26
 
27
27
  interface SubcommandGroup {
28
28
  readonly group: string | undefined
29
- readonly commands: Arr.NonEmptyReadonlyArray<Command<any, unknown, unknown, unknown>>
29
+ readonly commands: Arr.NonEmptyReadonlyArray<Command<any, unknown, any, unknown, unknown>>
30
30
  }
31
31
 
32
32
  /**
33
33
  * Internal implementation interface with all the machinery.
34
34
  * Use toImpl() to access from internal code.
35
35
  */
36
- export interface CommandInternal<Name extends string, Input, E, R> extends Command<Name, Input, E, R> {
36
+ export interface CommandInternal<Name extends string, Input, E, R, ContextInput>
37
+ extends Command<Name, Input, ContextInput, E, R>
38
+ {
37
39
  readonly config: ConfigInternal
38
- readonly service: ServiceMap.Key<CommandContext<Name>, Input>
40
+ readonly contextConfig: ConfigInternal
41
+ readonly service: ServiceMap.Key<CommandContext<Name>, ContextInput>
39
42
  readonly annotations: ServiceMap.ServiceMap<never>
40
43
  readonly globalFlags: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
41
44
  readonly parse: (input: ParsedTokens) => Effect.Effect<Input, CliError.CliError, Environment>
45
+ readonly parseContext: (input: ParsedTokens) => Effect.Effect<ContextInput, CliError.CliError, Environment>
42
46
  readonly handle: (
43
47
  input: Input,
44
48
  commandPath: ReadonlyArray<string>
@@ -60,9 +64,9 @@ export const TypeId = "~effect/cli/Command" as const
60
64
  * Casts a Command to its internal implementation.
61
65
  * For use by internal modules that need access to config, parse, handle, etc.
62
66
  */
63
- export const toImpl = <Name extends string, Input, E, R>(
64
- self: Command<Name, Input, E, R>
65
- ): CommandInternal<Name, Input, E, R> => self as CommandInternal<Name, Input, E, R>
67
+ export const toImpl = <Name extends string, Input, E, R, ContextInput = {}>(
68
+ self: Command<Name, Input, ContextInput, E, R>
69
+ ): CommandInternal<Name, Input, E, R, ContextInput> => self as CommandInternal<Name, Input, E, R, ContextInput>
66
70
 
67
71
  /* ========================================================================== */
68
72
  /* Proto */
@@ -73,7 +77,7 @@ export const Proto = {
73
77
  pipe() {
74
78
  return pipeArguments(this, arguments)
75
79
  },
76
- asEffect(this: Command<any, any, any, any>) {
80
+ asEffect(this: Command<any, any, any, any, any>) {
77
81
  return toImpl(this).service.asEffect()
78
82
  }
79
83
  }
@@ -85,10 +89,11 @@ export const Proto = {
85
89
  /**
86
90
  * Internal command constructor. Only accepts already-parsed ConfigInternal.
87
91
  */
88
- export const makeCommand = <const Name extends string, Input, E, R>(options: {
92
+ export const makeCommand = <const Name extends string, Input, E, R, ContextInput = {}>(options: {
89
93
  readonly name: Name
90
94
  readonly config: ConfigInternal
91
- readonly service?: ServiceMap.Key<CommandContext<Name>, Input> | undefined
95
+ readonly contextConfig?: ConfigInternal | undefined
96
+ readonly service?: ServiceMap.Key<CommandContext<Name>, ContextInput> | undefined
92
97
  readonly annotations?: ServiceMap.ServiceMap<never> | undefined
93
98
  readonly globalFlags?: ReadonlyArray<GlobalFlag.GlobalFlag<any>> | undefined
94
99
  readonly description?: string | undefined
@@ -97,12 +102,16 @@ export const makeCommand = <const Name extends string, Input, E, R>(options: {
97
102
  readonly examples?: ReadonlyArray<Command.Example> | undefined
98
103
  readonly subcommands?: ReadonlyArray<SubcommandGroup> | undefined
99
104
  readonly parse?: ((input: ParsedTokens) => Effect.Effect<Input, CliError.CliError, Environment>) | undefined
105
+ readonly parseContext?:
106
+ | ((input: ParsedTokens) => Effect.Effect<ContextInput, CliError.CliError, Environment>)
107
+ | undefined
100
108
  readonly handle?:
101
109
  | ((input: Input, commandPath: ReadonlyArray<string>) => Effect.Effect<void, E, R | Environment>)
102
110
  | undefined
103
- }): Command<Name, Input, E, R> => {
104
- const service = options.service ?? ServiceMap.Service<CommandContext<Name>, Input>(`${TypeId}/${options.name}`)
111
+ }): Command<Name, Input, ContextInput, E, R> => {
105
112
  const config = options.config
113
+ const contextConfig = options.contextConfig ?? emptyConfig
114
+ const service = options.service ?? ServiceMap.Service<CommandContext<Name>, ContextInput>(`${TypeId}/${options.name}`)
106
115
  const annotations = options.annotations ?? ServiceMap.empty()
107
116
  const globalFlags = options.globalFlags ?? []
108
117
  const subcommands = options.subcommands ?? []
@@ -113,13 +122,10 @@ export const makeCommand = <const Name extends string, Input, E, R>(options: {
113
122
  ): Effect.Effect<void, CliError.CliError | E, R | Environment> =>
114
123
  Predicate.isNotUndefined(options.handle)
115
124
  ? options.handle(input, commandPath)
116
- : Effect.fail(new CliError.ShowHelp({ commandPath }))
125
+ : Effect.fail(new CliError.ShowHelp({ commandPath, errors: [] }))
117
126
 
118
- const parse = options.parse ?? Effect.fnUntraced(function*(input: ParsedTokens) {
119
- const parsedArgs: Param.ParsedArgs = { flags: input.flags, arguments: input.arguments }
120
- const values = yield* parseParams(parsedArgs, config.orderedParams)
121
- return reconstructTree(config.tree, values) as Input
122
- })
127
+ const parse = options.parse ?? makeParser(config) as any
128
+ const parseContext = options.parseContext ?? makeParser(contextConfig) as any
123
129
 
124
130
  const buildHelpDoc = (commandPath: ReadonlyArray<string>): HelpDoc => {
125
131
  const args: Array<ArgDoc> = []
@@ -152,14 +158,7 @@ export const makeCommand = <const Name extends string, Input, E, R>(options: {
152
158
  for (const option of config.flags) {
153
159
  const singles = Param.extractSingleParams(option)
154
160
  for (const single of singles) {
155
- const formattedAliases = single.aliases.map((alias) => alias.length === 1 ? `-${alias}` : `--${alias}`)
156
- flags.push({
157
- name: single.name,
158
- aliases: formattedAliases,
159
- type: single.typeName ?? Primitive.getTypeName(single.primitiveType),
160
- description: single.description,
161
- required: single.primitiveType._tag !== "Boolean"
162
- })
161
+ flags.push(toFlagDoc(single))
163
162
  }
164
163
  }
165
164
 
@@ -198,8 +197,10 @@ export const makeCommand = <const Name extends string, Input, E, R>(options: {
198
197
  globalFlags,
199
198
  subcommands,
200
199
  config,
200
+ contextConfig,
201
201
  service,
202
202
  parse,
203
+ parseContext,
203
204
  handle,
204
205
  buildHelpDoc,
205
206
  ...(Predicate.isNotUndefined(options.description)
@@ -218,6 +219,32 @@ export const makeCommand = <const Name extends string, Input, E, R>(options: {
218
219
  /* Helpers */
219
220
  /* ========================================================================== */
220
221
 
222
+ /**
223
+ * Converts a single flag param into a FlagDoc for help display.
224
+ */
225
+ export const toFlagDoc = (single: Param.Single<typeof Param.flagKind, unknown>): FlagDoc => {
226
+ const formattedAliases = single.aliases.map((alias) => alias.length === 1 ? `-${alias}` : `--${alias}`)
227
+ return {
228
+ name: single.name,
229
+ aliases: formattedAliases,
230
+ type: single.typeName ?? Primitive.getTypeName(single.primitiveType),
231
+ description: single.description,
232
+ required: single.primitiveType._tag !== "Boolean"
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Creates a parser for a given config. Used as the default for both `parse`
238
+ * and `parseContext`, and also by `withSharedFlags` to avoid constructing a
239
+ * full throwaway command.
240
+ */
241
+ export const makeParser = (cfg: ConfigInternal) =>
242
+ Effect.fnUntraced(function*(input: ParsedTokens) {
243
+ const parsedArgs: Param.ParsedArgs = { flags: input.flags, arguments: input.arguments }
244
+ const values = yield* parseParams(parsedArgs, cfg.orderedParams)
245
+ return reconstructTree(cfg.tree, values)
246
+ })
247
+
221
248
  /**
222
249
  * Parses param values from parsed command arguments into their typed
223
250
  * representations.
@@ -245,15 +272,18 @@ const parseParams: (parsedArgs: Param.ParsedArgs, params: ReadonlyArray<Param.An
245
272
  /**
246
273
  * Checks for duplicate flag names between parent and child commands.
247
274
  */
248
- export const checkForDuplicateFlags = <Name extends string, Input>(
249
- parent: Command<Name, Input, unknown, unknown>,
250
- subcommands: ReadonlyArray<Command<any, unknown, unknown, unknown>>
275
+ export const checkForDuplicateFlags = <Name extends string, Input, ContextInput>(
276
+ parent: Command<Name, Input, ContextInput, unknown, unknown>,
277
+ subcommands: ReadonlyArray<Command<any, unknown, any, unknown, unknown>>,
278
+ options?: {
279
+ readonly contextConfig?: ConfigInternal | undefined
280
+ } | undefined
251
281
  ): void => {
252
282
  const parentImpl = toImpl(parent)
253
283
  const parentOptionNames = new Set<string>()
254
284
 
255
- const extractNames = (options: ReadonlyArray<Param.Any>): void => {
256
- for (const option of options) {
285
+ const extractNames = (flags: ReadonlyArray<Param.Any>): void => {
286
+ for (const option of flags) {
257
287
  const singles = Param.extractSingleParams(option)
258
288
  for (const single of singles) {
259
289
  parentOptionNames.add(single.name)
@@ -261,7 +291,7 @@ export const checkForDuplicateFlags = <Name extends string, Input>(
261
291
  }
262
292
  }
263
293
 
264
- extractNames(parentImpl.config.flags)
294
+ extractNames((options?.contextConfig ?? parentImpl.contextConfig).flags)
265
295
 
266
296
  for (const subcommand of subcommands) {
267
297
  const subImpl = toImpl(subcommand)
@@ -183,6 +183,55 @@ export const parseConfig = (config: Config): ConfigInternal => {
183
183
  }
184
184
  }
185
185
 
186
+ /** @internal */
187
+ export const emptyConfig: ConfigInternal = parseConfig({})
188
+
189
+ const shiftNodeIndexes = (node: ConfigInternal.Node, offset: number): ConfigInternal.Node => {
190
+ switch (node._tag) {
191
+ case "Param":
192
+ return {
193
+ _tag: "Param",
194
+ index: node.index + offset
195
+ }
196
+ case "Array":
197
+ return {
198
+ _tag: "Array",
199
+ children: node.children.map((child) => shiftNodeIndexes(child, offset))
200
+ }
201
+ case "Nested":
202
+ return {
203
+ _tag: "Nested",
204
+ tree: shiftTreeIndexes(node.tree, offset)
205
+ }
206
+ }
207
+ }
208
+
209
+ const shiftTreeIndexes = (tree: ConfigInternal.Tree, offset: number): ConfigInternal.Tree => {
210
+ const output: ConfigInternal.Tree = {}
211
+ for (const key in tree) {
212
+ output[key] = shiftNodeIndexes(tree[key], offset)
213
+ }
214
+ return output
215
+ }
216
+
217
+ /** @internal */
218
+ export const mergeConfig = (
219
+ left: ConfigInternal,
220
+ right: ConfigInternal
221
+ ): ConfigInternal => {
222
+ const offset = left.orderedParams.length
223
+ return {
224
+ [ConfigInternalTypeId]: ConfigInternalTypeId,
225
+ flags: [...left.flags, ...right.flags],
226
+ arguments: [...left.arguments, ...right.arguments],
227
+ orderedParams: [...left.orderedParams, ...right.orderedParams],
228
+ tree: {
229
+ ...left.tree,
230
+ ...shiftTreeIndexes(right.tree, offset)
231
+ }
232
+ }
233
+ }
234
+
186
235
  /* ========================================================================== */
187
236
  /* Reconstruction */
188
237
  /* ========================================================================== */
@@ -10,8 +10,7 @@ import type { Command } from "../Command.ts"
10
10
  import type * as GlobalFlag from "../GlobalFlag.ts"
11
11
  import type { FlagDoc, HelpDoc } from "../HelpDoc.ts"
12
12
  import * as Param from "../Param.ts"
13
- import * as Primitive from "../Primitive.ts"
14
- import { toImpl } from "./command.ts"
13
+ import { toFlagDoc, toImpl } from "./command.ts"
15
14
 
16
15
  const dedupeGlobalFlags = (
17
16
  flags: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
@@ -32,8 +31,8 @@ const dedupeGlobalFlags = (
32
31
  * Returns the resolved command lineage for the provided path.
33
32
  * Includes the root command as the first element.
34
33
  */
35
- export const getCommandsForCommandPath = <Name extends string, Input, E, R>(
36
- command: Command<Name, Input, E, R>,
34
+ export const getCommandsForCommandPath = <Name extends string, Input, E, R, ContextInput>(
35
+ command: Command<Name, Input, ContextInput, E, R>,
37
36
  commandPath: ReadonlyArray<string>
38
37
  ): ReadonlyArray<Command.Any> => {
39
38
  const commands: Array<Command.Any> = [command]
@@ -65,8 +64,8 @@ export const getCommandsForCommandPath = <Name extends string, Input, E, R>(
65
64
  * Returns active global flags for a command path.
66
65
  * Built-ins are prepended and declarations are collected root -> leaf.
67
66
  */
68
- export const getGlobalFlagsForCommandPath = <Name extends string, Input, E, R>(
69
- command: Command<Name, Input, E, R>,
67
+ export const getGlobalFlagsForCommandPath = <Name extends string, Input, E, R, ContextInput>(
68
+ command: Command<Name, Input, ContextInput, E, R>,
70
69
  commandPath: ReadonlyArray<string>,
71
70
  builtIns: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
72
71
  ): ReadonlyArray<GlobalFlag.GlobalFlag<any>> => {
@@ -97,12 +96,40 @@ const collectDeclaredGlobalFlags = (command: Command.Any): ReadonlyArray<GlobalF
97
96
  return dedupeGlobalFlags(collected)
98
97
  }
99
98
 
99
+ const getSharedFlagsForCommandPath = (
100
+ commands: ReadonlyArray<Command.Any>,
101
+ currentFlags: ReadonlyArray<FlagDoc>
102
+ ): ReadonlyArray<FlagDoc> => {
103
+ if (commands.length <= 1) {
104
+ return []
105
+ }
106
+
107
+ const seen = new Set(currentFlags.map((flag) => flag.name))
108
+ const sharedFlags: Array<FlagDoc> = []
109
+
110
+ for (const ancestor of commands.slice(0, -1)) {
111
+ const ancestorImpl = toImpl(ancestor)
112
+ for (const flag of ancestorImpl.contextConfig.flags) {
113
+ const singles = Param.extractSingleParams(flag)
114
+ for (const single of singles) {
115
+ if (seen.has(single.name)) {
116
+ continue
117
+ }
118
+ seen.add(single.name)
119
+ sharedFlags.push(toFlagDoc(single))
120
+ }
121
+ }
122
+ }
123
+
124
+ return sharedFlags
125
+ }
126
+
100
127
  /**
101
128
  * Returns all global flags declared in a command tree.
102
129
  * Built-ins are prepended and command declarations are deduplicated by identity.
103
130
  */
104
- export const getGlobalFlagsForCommandTree = <Name extends string, Input, E, R>(
105
- command: Command<Name, Input, E, R>,
131
+ export const getGlobalFlagsForCommandTree = <Name extends string, Input, E, R, ContextInput>(
132
+ command: Command<Name, Input, ContextInput, E, R>,
106
133
  builtIns: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
107
134
  ): ReadonlyArray<GlobalFlag.GlobalFlag<any>> =>
108
135
  dedupeGlobalFlags([
@@ -115,8 +142,8 @@ export const getGlobalFlagsForCommandTree = <Name extends string, Input, E, R>(
115
142
  * Navigates through the command hierarchy to find the right command.
116
143
  * Reads active global flags for the path and includes them in the help doc.
117
144
  */
118
- export const getHelpForCommandPath = <Name extends string, Input, E, R>(
119
- command: Command<Name, Input, E, R>,
145
+ export const getHelpForCommandPath = <Name extends string, Input, E, R, ContextInput>(
146
+ command: Command<Name, Input, ContextInput, E, R>,
120
147
  commandPath: ReadonlyArray<string>,
121
148
  builtIns: ReadonlyArray<GlobalFlag.GlobalFlag<any>>
122
149
  ): Effect.Effect<HelpDoc, never, never> =>
@@ -126,21 +153,19 @@ export const getHelpForCommandPath = <Name extends string, Input, E, R>(
126
153
 
127
154
  const baseDoc = toImpl(currentCommand).buildHelpDoc(commandPath)
128
155
 
156
+ const sharedFlags = getSharedFlagsForCommandPath(commands, baseDoc.flags)
157
+
129
158
  const flags = getGlobalFlagsForCommandPath(command, commandPath, builtIns)
130
159
  const globalFlagDocs: Array<FlagDoc> = []
131
160
  for (const flag of flags) {
132
161
  const singles = Param.extractSingleParams(flag.flag)
133
162
  for (const single of singles) {
134
- const formattedAliases = single.aliases.map((alias) => alias.length === 1 ? `-${alias}` : `--${alias}`)
135
163
  globalFlagDocs.push({
136
- name: single.name,
137
- aliases: formattedAliases,
138
- type: single.typeName ?? Primitive.getTypeName(single.primitiveType),
139
- description: single.description,
164
+ ...toFlagDoc(single),
140
165
  required: false
141
166
  })
142
167
  }
143
168
  }
144
169
 
145
- return { ...baseDoc, globalFlags: globalFlagDocs }
170
+ return { ...baseDoc, flags: [...sharedFlags, ...baseDoc.flags], globalFlags: globalFlagDocs }
146
171
  })
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Key Behaviors
22
22
  * -------------
23
- * - Parent flags may appear before OR after the subcommand name (npm-style)
23
+ * - Inherited parent flags may appear before OR after the subcommand name (npm-style)
24
24
  * - Only the first Value token can open a subcommand
25
25
  * - Errors accumulate rather than throwing exceptions
26
26
  */
@@ -58,10 +58,17 @@ export const parseArgs = (
58
58
  const flagParams = singles.filter(Param.isFlagParam)
59
59
  const flagRegistry = createFlagRegistry(flagParams)
60
60
 
61
+ const inheritedSingles = commandImpl.contextConfig.flags.flatMap(Param.extractSingleParams)
62
+ const inheritedFlagParams = inheritedSingles.filter(Param.isFlagParam)
63
+ const inheritedFlagRegistry = createFlagRegistry(inheritedFlagParams)
64
+ const inheritedNames = new Set(inheritedFlagParams.map((param) => param.name))
65
+
61
66
  const context: CommandContext = {
62
67
  command,
63
68
  commandPath: newCommandPath,
64
- flagRegistry
69
+ flagRegistry,
70
+ inheritedFlagRegistry,
71
+ localFlagNames: flagParams.filter((param) => !inheritedNames.has(param.name)).map((param) => param.name)
65
72
  }
66
73
 
67
74
  const result = scanCommandLevel(tokens, context)
@@ -133,6 +140,8 @@ type CommandContext = {
133
140
  readonly command: Command.Any
134
141
  readonly commandPath: ReadonlyArray<string>
135
142
  readonly flagRegistry: FlagRegistry
143
+ readonly inheritedFlagRegistry: FlagRegistry
144
+ readonly localFlagNames: ReadonlyArray<string>
136
145
  }
137
146
 
138
147
  /**
@@ -176,7 +185,7 @@ type LeafResult = {
176
185
  type SubcommandResult = {
177
186
  readonly _tag: "Sub"
178
187
  readonly flags: Readonly<FlagMap>
179
- readonly sub: Command<string, unknown, unknown, unknown>
188
+ readonly sub: Command<string, unknown, unknown, unknown, unknown>
180
189
  readonly childTokens: ReadonlyArray<Token>
181
190
  readonly errors: ReadonlyArray<CliError.CliError>
182
191
  }
@@ -242,9 +251,9 @@ export const createFlagRegistry = (params: ReadonlyArray<FlagParam>): FlagRegist
242
251
 
243
252
  const buildSubcommandIndex = (
244
253
  subcommands: Command.Any["subcommands"]
245
- ): Map<string, Command<string, unknown, unknown, unknown>> => {
246
- const index = new Map<string, Command<string, unknown, unknown, unknown>>()
247
- const setKey = (key: string, command: Command<string, unknown, unknown, unknown>) => {
254
+ ): Map<string, Command<string, unknown, unknown, unknown, unknown>> => {
255
+ const index = new Map<string, Command<string, unknown, unknown, unknown, unknown>>()
256
+ const setKey = (key: string, command: Command<string, unknown, unknown, unknown, unknown>) => {
248
257
  const existing = index.get(key)
249
258
  if (existing && existing !== command) {
250
259
  throw new Error(
@@ -448,7 +457,7 @@ const toLeafResult = (state: ParseState): LeafResult => ({
448
457
  * Determines how to handle the first value token.
449
458
  *
450
459
  * If it matches a known subcommand:
451
- * - Collect any parent flags from remaining tokens (npm-style)
460
+ * - Collect inherited parent flags from remaining tokens (npm-style)
452
461
  * - Return SubcommandResult with child tokens
453
462
  *
454
463
  * Otherwise:
@@ -461,13 +470,30 @@ const resolveFirstValue = (
461
470
  context: CommandContext,
462
471
  state: ParseState
463
472
  ): FirstValueResult => {
464
- const { command, commandPath, flagRegistry } = context
473
+ const { command, commandPath, inheritedFlagRegistry, localFlagNames } = context
465
474
  const subIndex = buildSubcommandIndex(command.subcommands)
466
475
  const sub = subIndex.get(value)
467
476
 
468
477
  if (sub) {
469
- // npm-style: parent flags can appear after subcommand name
470
- const tail = consumeKnownFlags(cursor.rest(), flagRegistry)
478
+ const selectedPath = [...commandPath, sub.name]
479
+
480
+ // Local flags are not inherited by subcommands.
481
+ const parentFlags = state.flags.snapshot()
482
+ for (const localFlagName of localFlagNames) {
483
+ const values = parentFlags[localFlagName]
484
+ if (values !== undefined && values.length > 0) {
485
+ state.errors.push(
486
+ new CliError.UnrecognizedOption({
487
+ option: `--${localFlagName}`,
488
+ suggestions: [],
489
+ command: selectedPath
490
+ })
491
+ )
492
+ }
493
+ }
494
+
495
+ // npm-style: inherited parent flags can appear after subcommand name
496
+ const tail = consumeKnownFlags(cursor.rest(), inheritedFlagRegistry)
471
497
  state.flags.merge(tail.flagMap)
472
498
 
473
499
  return {
@@ -36,7 +36,6 @@ import * as Multipart from "../http/Multipart.ts"
36
36
  import * as UrlParams from "../http/UrlParams.ts"
37
37
  import type * as HttpApi from "./HttpApi.ts"
38
38
  import * as HttpApiEndpoint from "./HttpApiEndpoint.ts"
39
- import { HttpApiSchemaError } from "./HttpApiError.ts"
40
39
  import type * as HttpApiGroup from "./HttpApiGroup.ts"
41
40
  import * as HttpApiMiddleware from "./HttpApiMiddleware.ts"
42
41
  import * as HttpApiSchema from "./HttpApiSchema.ts"
@@ -598,12 +597,7 @@ function handlerToHttpEffect(
598
597
  })
599
598
  ).pipe(
600
599
  Effect.withErrorReporting,
601
- Effect.catch((error) => {
602
- if (Schema.isSchemaError(error)) {
603
- error = HttpApiSchemaError.fromSchemaError(error)
604
- }
605
- return Effect.orDie(encodeError(error))
606
- }),
600
+ Effect.catch((error) => Effect.orDie(encodeError(error))),
607
601
  Effect.provideServices(services)
608
602
  )
609
603
  }
@@ -23,7 +23,7 @@ import * as HttpMethod from "../http/HttpMethod.ts"
23
23
  import * as UrlParams from "../http/UrlParams.ts"
24
24
  import * as HttpApi from "./HttpApi.ts"
25
25
  import * as HttpApiEndpoint from "./HttpApiEndpoint.ts"
26
- import type { HttpApiSchemaError } from "./HttpApiError.ts"
26
+ import type { BadRequest } from "./HttpApiError.ts"
27
27
  import type * as HttpApiGroup from "./HttpApiGroup.ts"
28
28
  import type * as HttpApiMiddleware from "./HttpApiMiddleware.ts"
29
29
  import * as HttpApiSchema from "./HttpApiSchema.ts"
@@ -32,7 +32,7 @@ import * as HttpApiSchema from "./HttpApiSchema.ts"
32
32
  * @since 4.0.0
33
33
  * @category models
34
34
  */
35
- export type Client<Groups extends HttpApiGroup.Any, E = HttpApiSchemaError, R = never> = Simplify<
35
+ export type Client<Groups extends HttpApiGroup.Any, E = BadRequest, R = never> = Simplify<
36
36
  & {
37
37
  readonly [Group in Extract<Groups, { readonly topLevel: false }> as HttpApiGroup.Name<Group>]: Client.Group<
38
38
  Group,
@@ -50,7 +50,7 @@ export type Client<Groups extends HttpApiGroup.Any, E = HttpApiSchemaError, R =
50
50
  * @since 4.0.0
51
51
  * @category models
52
52
  */
53
- export type ForApi<Api extends HttpApi.Any, E = HttpApiSchemaError, R = never> = Api extends
53
+ export type ForApi<Api extends HttpApi.Any, E = BadRequest, R = never> = Api extends
54
54
  HttpApi.HttpApi<infer _Id, infer Groups> ? Client<Groups, E, R> :
55
55
  never
56
56
 
@@ -383,7 +383,7 @@ export const makeWith = <ApiId extends string, Groups extends HttpApiGroup.Any,
383
383
  | undefined
384
384
  readonly baseUrl?: URL | string | undefined
385
385
  }
386
- ): Effect.Effect<Client<Groups, HttpApiSchemaError | E, R>, never, HttpApiGroup.MiddlewareClient<Groups>> => {
386
+ ): Effect.Effect<Client<Groups, BadRequest | E, R>, never, HttpApiGroup.MiddlewareClient<Groups>> => {
387
387
  const client: Record<string, Record<string, any>> = {}
388
388
  return makeClient(api, {
389
389
  ...options,
@@ -418,7 +418,7 @@ export const group = <
418
418
  readonly baseUrl?: URL | string | undefined
419
419
  }
420
420
  ): Effect.Effect<
421
- Client.Group<Groups, GroupName, HttpApiSchemaError | E, R>,
421
+ Client.Group<Groups, GroupName, BadRequest | E, R>,
422
422
  never,
423
423
  HttpApiGroup.MiddlewareClient<HttpApiGroup.WithName<Groups, GroupName>>
424
424
  > => {
@@ -458,7 +458,7 @@ export const endpoint = <
458
458
  ): Effect.Effect<
459
459
  Client.Method<
460
460
  HttpApiEndpoint.WithName<HttpApiGroup.Endpoints<HttpApiGroup.WithName<Groups, GroupName>>, EndpointName>,
461
- HttpApiSchemaError | E,
461
+ BadRequest | E,
462
462
  R
463
463
  >,
464
464
  never,
@@ -15,7 +15,7 @@ import * as HttpRouter from "../http/HttpRouter.ts"
15
15
  import type { HttpServerRequest } from "../http/HttpServerRequest.ts"
16
16
  import type { HttpServerResponse } from "../http/HttpServerResponse.ts"
17
17
  import type * as Multipart from "../http/Multipart.ts"
18
- import { HttpApiSchemaError } from "./HttpApiError.ts"
18
+ import { BadRequestFromSchemaError, type BadRequestNoContent } from "./HttpApiError.ts"
19
19
  import type * as HttpApiGroup from "./HttpApiGroup.ts"
20
20
  import type * as HttpApiMiddleware from "./HttpApiMiddleware.ts"
21
21
  import * as HttpApiSchema from "./HttpApiSchema.ts"
@@ -53,7 +53,7 @@ export interface HttpApiEndpoint<
53
53
  out Payload extends Schema.Top = never,
54
54
  out Headers extends Schema.Top = never,
55
55
  out Success extends Schema.Top = typeof HttpApiSchema.NoContent,
56
- out Error extends Schema.Top = typeof HttpApiSchemaError,
56
+ out Error extends Schema.Top = typeof BadRequestNoContent,
57
57
  in out Middleware = never,
58
58
  out MiddlewareR = never
59
59
  > extends Pipeable {
@@ -179,7 +179,7 @@ export function getErrorSchemas(endpoint: AnyWithProps): [Schema.Top, ...Array<S
179
179
  schemas.add(key.error)
180
180
  }
181
181
  }
182
- return Arr.append(Array.from(schemas), HttpApiSchemaError)
182
+ return Arr.append(Array.from(schemas), BadRequestFromSchemaError)
183
183
  }
184
184
 
185
185
  /**
@@ -936,7 +936,7 @@ export const make = <Method extends HttpMethod>(method: Method) =>
936
936
  : Payload,
937
937
  Headers extends Schema.Struct.Fields ? Schema.Struct<Headers> : Headers,
938
938
  Success extends ReadonlyArray<Schema.Top> ? Success[number] : Success,
939
- (Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error) | typeof HttpApiSchemaError
939
+ (Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error) | typeof BadRequestNoContent
940
940
  > => {
941
941
  return makeProto({
942
942
  name,
@@ -2,30 +2,11 @@
2
2
  * @since 4.0.0
3
3
  */
4
4
  import * as ErrorReporter from "../../ErrorReporter.ts"
5
+ import { identity } from "../../Function.ts"
5
6
  import * as Schema from "../../Schema.ts"
7
+ import * as Transformation from "../../SchemaTransformation.ts"
6
8
  import * as HttpApiSchema from "./HttpApiSchema.ts"
7
9
 
8
- /**
9
- * @category errors
10
- * @since 4.0.0
11
- */
12
- export class HttpApiSchemaError extends Schema.ErrorClass<HttpApiSchemaError>("effect/HttpApiSchemaError")({
13
- _tag: Schema.tag("HttpApiSchemaError"),
14
- message: Schema.String
15
- }, {
16
- httpApiStatus: 400,
17
- description: "The request or response did not match the expected schema"
18
- }) {
19
- /**
20
- * @since 4.0.0
21
- */
22
- static fromSchemaError(error: Schema.SchemaError): HttpApiSchemaError {
23
- return new HttpApiSchemaError({ message: error.message })
24
- }
25
-
26
- override readonly [ErrorReporter.ignore] = true
27
- }
28
-
29
10
  /**
30
11
  * @category Built-in errors
31
12
  * @since 4.0.0
@@ -37,6 +18,7 @@ export class BadRequest extends Schema.ErrorClass<BadRequest>("effect/HttpApiErr
37
18
  httpApiStatus: 400
38
19
  }) {
39
20
  override readonly [ErrorReporter.ignore] = true
21
+ static readonly singleton = new BadRequest()
40
22
  }
41
23
 
42
24
  /**
@@ -47,6 +29,26 @@ export const BadRequestNoContent = BadRequest.pipe(HttpApiSchema.asNoContent({
47
29
  decode: () => new BadRequest({})
48
30
  }))
49
31
 
32
+ /**
33
+ * @category Built-in errors
34
+ * @since 4.0.0
35
+ */
36
+ export const BadRequestFromSchemaError = BadRequest.pipe(
37
+ Schema.decodeTo(
38
+ Schema.Union([Schema.declare(Schema.isSchemaError), BadRequest]),
39
+ Transformation.transform({
40
+ encode: (_) => BadRequest.singleton,
41
+ decode: identity
42
+ })
43
+ ),
44
+ HttpApiSchema.asNoContent({
45
+ decode: () => new BadRequest({})
46
+ })
47
+ ).annotate({
48
+ httpApiStatus: 400,
49
+ description: "BadRequest"
50
+ })
51
+
50
52
  /**
51
53
  * @category Built-in errors
52
54
  * @since 4.0.0