Skip to content

Commit c414700

Browse files
authored
Make CLI global settings directly yieldable (Effect-TS#1471)
1 parent 1da0fc3 commit c414700

6 files changed

Lines changed: 267 additions & 186 deletions

File tree

.changeset/few-mirrors-pull.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Make CLI global settings directly yieldable and simplify built-in names.
6+
7+
`GlobalFlag.setting` now takes `{ flag, defaultValue }` and returns a setting that is a `ServiceMap.Reference`, so handlers and `Command.provide*` effects can `yield*` global setting values directly.
8+
9+
Built-in settings keep internal behavior in `runWith` (for example, `--log-level` still configures `References.MinimumLogLevel`) while also being readable as values.
10+
11+
Also renamed built-in globals:
12+
13+
- `GlobalFlag.CompletionsFlag` -> `GlobalFlag.Completions`
14+
- `GlobalFlag.LogLevelFlag` -> `GlobalFlag.LogLevel`

packages/effect/src/unstable/cli/Command.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import * as Effect from "../../Effect.ts"
77
import type * as FileSystem from "../../FileSystem.ts"
88
import { dual } from "../../Function.ts"
99
import * as Layer from "../../Layer.ts"
10+
import * as Option from "../../Option.ts"
1011
import type * as Path from "../../Path.ts"
1112
import type { Pipeable } from "../../Pipeable.ts"
1213
import * as Predicate from "../../Predicate.ts"
14+
import * as References from "../../References.ts"
1315
import * as Result from "../../Result.ts"
1416
import * as ServiceMap from "../../ServiceMap.ts"
1517
import * as Terminal from "../../Terminal.ts"
@@ -1111,12 +1113,8 @@ export const runWith = <const Name extends string, Input, E, R>(
11111113
function*(args: ReadonlyArray<string>) {
11121114
const { tokens, trailingOperands } = Lexer.lex(args)
11131115

1114-
// 1. Read registry and resolve each reference to its current value
1115-
const refs = yield* GlobalFlag.Registry
1116-
const flags: Array<GlobalFlag.GlobalFlag<any>> = []
1117-
for (const ref of refs) {
1118-
flags.push(yield* ref)
1119-
}
1116+
// 1. Read global flags from registry
1117+
const flags = Array.from(yield* GlobalFlag.Registry)
11201118

11211119
// 2. Extract global flag tokens
11221120
const allFlagParams = flags.flatMap((f) => Param.extractSingleParams(f.flag))
@@ -1152,15 +1150,27 @@ export const runWith = <const Name extends string, Input, E, R>(
11521150
return yield* showHelp(command, commandPath, [parseResult.failure])
11531151
}
11541152

1155-
// 6. Compose setting flag layers
1153+
// 6. Provide setting values
11561154
let contextLayer: Layer.Layer<never> = Layer.empty
11571155
for (const flag of flags) {
11581156
if (flag._tag !== "Setting") continue
11591157
const [, value] = yield* flag.flag.parse(emptyArgs)
1160-
contextLayer = Layer.merge(contextLayer, flag.layer(value))
1158+
contextLayer = Layer.merge(contextLayer, Layer.succeed(flag, value))
1159+
}
1160+
1161+
// 7. Apply built-in setting behavior
1162+
if (flags.includes(GlobalFlag.LogLevel)) {
1163+
const [, logLevel] = yield* GlobalFlag.LogLevel.flag.parse(emptyArgs)
1164+
contextLayer = Layer.merge(
1165+
contextLayer,
1166+
Option.match(logLevel, {
1167+
onNone: () => Layer.empty,
1168+
onSome: (level) => Layer.succeed(References.MinimumLogLevel, level)
1169+
})
1170+
)
11611171
}
11621172

1163-
// 7. Run command handler with composed context
1173+
// 8. Run command handler with composed context
11641174
const program = commandImpl.handle(parseResult.success, [command.name])
11651175
yield* Effect.provide(program, contextLayer)
11661176
},

packages/effect/src/unstable/cli/GlobalFlag.ts

Lines changed: 86 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
import * as Console from "../../Console.ts"
66
import * as Effect from "../../Effect.ts"
77
import { dual } from "../../Function.ts"
8-
import * as Layer from "../../Layer.ts"
9-
import type { LogLevel } from "../../LogLevel.ts"
8+
import type { LogLevel as LogLevelType } from "../../LogLevel.ts"
109
import * as Option from "../../Option.ts"
11-
import * as References from "../../References.ts"
1210
import * as ServiceMap from "../../ServiceMap.ts"
1311
import * as CliOutput from "./CliOutput.ts"
1412
import type * as Command from "./Command.ts"
@@ -51,10 +49,9 @@ export interface Action<A> {
5149
* @since 4.0.0
5250
* @category models
5351
*/
54-
export interface Setting<A> {
52+
export interface Setting<A> extends ServiceMap.Reference<A> {
5553
readonly _tag: "Setting"
5654
readonly flag: Flag.Flag<A>
57-
readonly layer: (value: A) => Layer.Layer<never>
5855
}
5956

6057
/**
@@ -95,19 +92,27 @@ export const action = <A>(options: {
9592
*/
9693
export const setting = <A>(options: {
9794
readonly flag: Flag.Flag<A>
98-
readonly layer: (value: A) => Layer.Layer<never>
99-
}): Setting<A> => ({
100-
_tag: "Setting",
101-
flag: options.flag,
102-
layer: options.layer
103-
})
95+
readonly defaultValue: () => A
96+
}): Setting<A> => {
97+
settingIdCounter += 1
98+
const ref = ServiceMap.Reference<A>(
99+
`effect/cli/GlobalFlag/Setting/${settingIdCounter}`,
100+
{ defaultValue: options.defaultValue }
101+
)
102+
return Object.assign(ref, {
103+
_tag: "Setting" as const,
104+
flag: options.flag
105+
})
106+
}
107+
108+
let settingIdCounter = 0
104109

105110
/* ========================================================================== */
106111
/* Built-in Flag References */
107112
/* ========================================================================== */
108113

109114
import * as CommandDescriptor from "./internal/completions/CommandDescriptor.ts"
110-
import * as Completions from "./internal/completions/Completions.ts"
115+
import * as CompletionsInternal from "./internal/completions/Completions.ts"
111116
import * as HelpInternal from "./internal/help.ts"
112117

113118
/**
@@ -117,24 +122,18 @@ import * as HelpInternal from "./internal/help.ts"
117122
* @since 4.0.0
118123
* @category references
119124
*/
120-
export const Help: ServiceMap.Reference<Action<boolean>> = ServiceMap.Reference(
121-
"effect/cli/GlobalFlag/Help",
122-
{
123-
defaultValue: () =>
124-
action({
125-
flag: Flag.boolean("help").pipe(
126-
Flag.withAlias("h"),
127-
Flag.withDescription("Show help information")
128-
),
129-
run: (_, { command, commandPath }) =>
130-
Effect.gen(function*() {
131-
const formatter = yield* CliOutput.Formatter
132-
const helpDoc = yield* HelpInternal.getHelpForCommandPath(command, commandPath, Registry)
133-
yield* Console.log(formatter.formatHelpDoc(helpDoc))
134-
})
135-
})
136-
}
137-
)
125+
export const Help: Action<boolean> = action({
126+
flag: Flag.boolean("help").pipe(
127+
Flag.withAlias("h"),
128+
Flag.withDescription("Show help information")
129+
),
130+
run: (_, { command, commandPath }) =>
131+
Effect.gen(function*() {
132+
const formatter = yield* CliOutput.Formatter
133+
const helpDoc = yield* HelpInternal.getHelpForCommandPath(command, commandPath, Registry)
134+
yield* Console.log(formatter.formatHelpDoc(helpDoc))
135+
})
136+
})
138137

139138
/**
140139
* The `--version` global flag.
@@ -143,22 +142,16 @@ export const Help: ServiceMap.Reference<Action<boolean>> = ServiceMap.Reference(
143142
* @since 4.0.0
144143
* @category references
145144
*/
146-
export const Version: ServiceMap.Reference<Action<boolean>> = ServiceMap.Reference(
147-
"effect/cli/GlobalFlag/Version",
148-
{
149-
defaultValue: () =>
150-
action({
151-
flag: Flag.boolean("version").pipe(
152-
Flag.withDescription("Show version information")
153-
),
154-
run: (_, { command, version }) =>
155-
Effect.gen(function*() {
156-
const formatter = yield* CliOutput.Formatter
157-
yield* Console.log(formatter.formatVersion(command.name, version))
158-
})
159-
})
160-
}
161-
)
145+
export const Version: Action<boolean> = action({
146+
flag: Flag.boolean("version").pipe(
147+
Flag.withDescription("Show version information")
148+
),
149+
run: (_, { command, version }) =>
150+
Effect.gen(function*() {
151+
const formatter = yield* CliOutput.Formatter
152+
yield* Console.log(formatter.formatVersion(command.name, version))
153+
})
154+
})
162155

163156
/**
164157
* The `--completions` global flag.
@@ -167,25 +160,20 @@ export const Version: ServiceMap.Reference<Action<boolean>> = ServiceMap.Referen
167160
* @since 4.0.0
168161
* @category references
169162
*/
170-
export const CompletionsFlag: ServiceMap.Reference<
171-
Action<Option.Option<"bash" | "zsh" | "fish">>
172-
> = ServiceMap.Reference("effect/cli/GlobalFlag/Completions", {
173-
defaultValue: () =>
174-
action({
175-
flag: Flag.choice("completions", ["bash", "zsh", "fish", "sh"] as const)
176-
.pipe(
177-
Flag.optional,
178-
Flag.map((v) => Option.map(v, (s) => s === "sh" ? "bash" : s)),
179-
Flag.withDescription("Print shell completion script")
180-
),
181-
run: (shell, { command }) =>
182-
Effect.gen(function*() {
183-
if (Option.isNone(shell)) return
184-
const descriptor = CommandDescriptor.fromCommand(command)
185-
yield* Console.log(
186-
Completions.generate(command.name, shell.value, descriptor)
187-
)
188-
})
163+
export const Completions: Action<Option.Option<"bash" | "zsh" | "fish">> = action({
164+
flag: Flag.choice("completions", ["bash", "zsh", "fish", "sh"] as const)
165+
.pipe(
166+
Flag.optional,
167+
Flag.map((v) => Option.map(v, (s) => s === "sh" ? "bash" : s)),
168+
Flag.withDescription("Print shell completion script")
169+
),
170+
run: (shell, { command }) =>
171+
Effect.gen(function*() {
172+
if (Option.isNone(shell)) return
173+
const descriptor = CommandDescriptor.fromCommand(command)
174+
yield* Console.log(
175+
CompletionsInternal.generate(command.name, shell.value, descriptor)
176+
)
189177
})
190178
})
191179

@@ -196,34 +184,25 @@ export const CompletionsFlag: ServiceMap.Reference<
196184
* @since 4.0.0
197185
* @category references
198186
*/
199-
export const LogLevelFlag: ServiceMap.Reference<
200-
Setting<Option.Option<LogLevel>>
201-
> = ServiceMap.Reference("effect/cli/GlobalFlag/LogLevel", {
202-
defaultValue: () =>
203-
setting({
204-
flag: Flag.choiceWithValue(
205-
"log-level",
206-
[
207-
["all", "All"],
208-
["trace", "Trace"],
209-
["debug", "Debug"],
210-
["info", "Info"],
211-
["warn", "Warn"],
212-
["warning", "Warn"],
213-
["error", "Error"],
214-
["fatal", "Fatal"],
215-
["none", "None"]
216-
] as const
217-
).pipe(
218-
Flag.optional,
219-
Flag.withDescription("Sets the minimum log level")
220-
),
221-
layer: (value) =>
222-
Option.match(value, {
223-
onNone: () => Layer.empty,
224-
onSome: (level) => Layer.succeed(References.MinimumLogLevel, level)
225-
})
226-
})
187+
export const LogLevel: Setting<Option.Option<LogLevelType>> = setting({
188+
flag: Flag.choiceWithValue(
189+
"log-level",
190+
[
191+
["all", "All"],
192+
["trace", "Trace"],
193+
["debug", "Debug"],
194+
["info", "Info"],
195+
["warn", "Warn"],
196+
["warning", "Warn"],
197+
["error", "Error"],
198+
["fatal", "Fatal"],
199+
["none", "None"]
200+
] as const
201+
).pipe(
202+
Flag.optional,
203+
Flag.withDescription("Sets the minimum log level")
204+
),
205+
defaultValue: () => Option.none()
227206
})
228207

229208
/* ========================================================================== */
@@ -238,14 +217,14 @@ export const LogLevelFlag: ServiceMap.Reference<
238217
* @category references
239218
*/
240219
export const Registry: ServiceMap.Reference<
241-
Set<ServiceMap.Reference<GlobalFlag<any>>>
220+
Set<GlobalFlag<any>>
242221
> = ServiceMap.Reference("effect/cli/GlobalFlag/Registry", {
243222
defaultValue: () =>
244-
new Set([
245-
Help as ServiceMap.Reference<GlobalFlag<any>>,
246-
Version as ServiceMap.Reference<GlobalFlag<any>>,
247-
CompletionsFlag as ServiceMap.Reference<GlobalFlag<any>>,
248-
LogLevelFlag as ServiceMap.Reference<GlobalFlag<any>>
223+
new Set<GlobalFlag<any>>([
224+
Help,
225+
Version,
226+
Completions,
227+
LogLevel
249228
])
250229
})
251230

@@ -261,22 +240,22 @@ export const Registry: ServiceMap.Reference<
261240
*/
262241
export const add: {
263242
<A>(
264-
ref: ServiceMap.Reference<GlobalFlag<A>>
243+
flag: GlobalFlag<A>
265244
): <B, E, R>(
266245
self: Effect.Effect<B, E, R>
267246
) => Effect.Effect<B, E, R>
268247
<B, E, R, A>(
269248
self: Effect.Effect<B, E, R>,
270-
ref: ServiceMap.Reference<GlobalFlag<A>>
249+
flag: GlobalFlag<A>
271250
): Effect.Effect<B, E, R>
272251
} = dual(
273252
2,
274253
Effect.fnUntraced(function*<B, E, R, A>(
275254
self: Effect.Effect<B, E, R>,
276-
ref: ServiceMap.Reference<GlobalFlag<A>>
255+
flag: GlobalFlag<A>
277256
) {
278257
const currentRegistry = yield* Registry
279-
const nextRegistry = new Set([...currentRegistry, ref])
258+
const nextRegistry = new Set([...currentRegistry, flag])
280259
return yield* Effect.provideService(self, Registry, nextRegistry)
281260
})
282261
)
@@ -289,23 +268,23 @@ export const add: {
289268
*/
290269
export const remove: {
291270
<A>(
292-
ref: ServiceMap.Reference<GlobalFlag<A>>
271+
flag: GlobalFlag<A>
293272
): <B, E, R>(
294273
self: Effect.Effect<B, E, R>
295274
) => Effect.Effect<B, E, R>
296275
<B, E, R, A>(
297276
self: Effect.Effect<B, E, R>,
298-
ref: ServiceMap.Reference<GlobalFlag<A>>
277+
flag: GlobalFlag<A>
299278
): Effect.Effect<B, E, R>
300279
} = dual(
301280
2,
302281
Effect.fnUntraced(function*<B, E, R, A>(
303282
self: Effect.Effect<B, E, R>,
304-
ref: ServiceMap.Reference<GlobalFlag<A>>
283+
flag: GlobalFlag<A>
305284
) {
306285
const currentRegistry = yield* Registry
307286
const nextRegistry = new Set(currentRegistry)
308-
nextRegistry.delete(ref)
287+
nextRegistry.delete(flag)
309288
return yield* Effect.provideService(self, Registry, nextRegistry)
310289
})
311290
)

packages/effect/src/unstable/cli/internal/help.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { toImpl } from "./command.ts"
2222
export const getHelpForCommandPath = <Name extends string, Input, E, R>(
2323
command: Command<Name, Input, E, R>,
2424
commandPath: ReadonlyArray<string>,
25-
registry: ServiceMap.Reference<Set<ServiceMap.Reference<GlobalFlag<unknown>>>>
25+
registry: ServiceMap.Reference<Set<GlobalFlag<unknown>>>
2626
): Effect.Effect<HelpDoc, never, never> =>
2727
Effect.gen(function*() {
2828
let currentCommand: Command.Any = command
@@ -45,10 +45,9 @@ export const getHelpForCommandPath = <Name extends string, Input, E, R>(
4545

4646
const baseDoc = toImpl(currentCommand).buildHelpDoc(commandPath)
4747

48-
const refs = yield* registry
48+
const flags = yield* registry
4949
const globalFlagDocs: Array<FlagDoc> = []
50-
for (const ref of refs) {
51-
const flag = yield* ref
50+
for (const flag of flags) {
5251
const singles = Param.extractSingleParams(flag.flag)
5352
for (const single of singles) {
5453
const formattedAliases = single.aliases.map((alias) => alias.length === 1 ? `-${alias}` : `--${alias}`)

0 commit comments

Comments
 (0)