1/**
2 * Parses raw command-line strings into typed values.
3 *
4 * A `Primitive<A>` receives one string and returns an `Effect` that either
5 * produces an `A` or fails with a parser message. `Argument` and `Flag` build
6 * on these primitives to add names, aliases, defaults, prompts, configuration
7 * fallbacks, repetition, and help metadata. Primitive parsers cover common
8 * scalar values, paths, files, structured config files, schema-decoded input,
9 * redacted values, and key-value pairs.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Effect from "../Effect.ts"
15import * as Ini from "../encoding/Ini.ts"
16import * as Toml from "../encoding/Toml.ts"
17import * as Yaml from "../encoding/Yaml.ts"
18import * as FileSystem from "../FileSystem.ts"
19import { format } from "../Formatter.ts"
20import { identity } from "../Function.ts"
21import * as Path_ from "../Path.ts"
22import * as Redacted_ from "../Redacted.ts"
23import * as Schema from "../Schema.ts"
24import type { Formatter } from "../SchemaIssue.ts"
25import type * as Struct from "../Struct.ts"
26import type { Covariant } from "../Types.ts"
27import type { Environment } from "./Command.ts"
28
29const TypeId = "~effect/cli/Primitive"
30
31/**
32 * Represents a primitive type that can parse string input into a typed value.
33 *
34 * **Example** (Parsing values with primitives)
35 *
36 * ```ts import.meta.vitest
37 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
38 * import { Primitive } from "effect/cli"
39 * import { ChildProcessSpawner } from "effect/process"
40 *
41 * const CliTestLayer = Layer.mergeAll(
42 * FileSystem.layerNoop({}),
43 * Path.layer,
44 * Stdio.layerTest({}),
45 * Layer.succeed(Terminal.Terminal, Terminal.make({
46 * columns: Effect.succeed(80),
47 * rows: Effect.succeed(24),
48 * readInput: Effect.die("unused"),
49 * readLine: Effect.die("unused"),
50 * display: () => Effect.void
51 * })),
52 * Layer.succeed(
53 * ChildProcessSpawner.ChildProcessSpawner,
54 * ChildProcessSpawner.make(() => Effect.die("unused"))
55 * )
56 * )
57 *
58 * const program = Effect.gen(function*() {
59 * const stringResult = yield* Primitive.String.parse("hello")
60 * const numberResult = yield* Primitive.Int.parse("42")
61 * const boolResult = yield* Primitive.Boolean.parse("true")
62 * return [stringResult, numberResult, boolResult] as const
63 * })
64 *
65 * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer))) // => ["hello", 42, true]
66 * ```
67 *
68 * @stability unstable
69 * @category models
70 * @since 4.0.0
71 */
72export interface Primitive<out A> extends Primitive.Variance<A> {
73 readonly _tag: string
74 readonly parse: (value: string) => Effect.Effect<A, string, Environment>
75}
76
77/**
78 * Namespace containing type-level helpers for `Primitive`.
79 *
80 * @stability unstable
81 * @since 4.0.0
82 */
83export declare namespace Primitive {
84 /**
85 * Type-level variance marker for the value parsed by a `Primitive`.
86 *
87 * @stability unstable
88 * @category models
89 * @since 4.0.0
90 */
91 export interface Variance<out A> {
92 readonly [TypeId]: {
93 readonly _A: Covariant<A>
94 }
95 }
96}
97
98const Proto = {
99 [TypeId]: {
100 _A: identity
101 }
102}
103
104/**
105 * @internal
106 */
107export const isTrueLiteral = Schema.is(Schema.TrueLiterals)
108
109/**
110 * @internal
111 */
112export const isFalseLiteral = Schema.is(Schema.FalseLiterals)
113
114/**
115 * @internal
116 */
117export const isBoolean = (p: Primitive<unknown>): p is Primitive<boolean> => p._tag === "Boolean"
118
119const makePrimitive = <A>(
120 tag: string,
121 parse: (value: string) => Effect.Effect<A, string, Environment>
122): Primitive<A> =>
123 Object.assign(Object.create(Proto), {
124 _tag: tag,
125 parse
126 })
127
128const makeSchemaPrimitive = <T>(
129 tag: string,
130 schema: Schema.ConstraintDecoder<T, Environment>
131): Primitive<T> => {
132 const toCodecStringTree = Schema.toCodecStringTree(schema)
133 const decode = Schema.decodeUnknownEffect(toCodecStringTree)
134 return makePrimitive(tag, (value) => Effect.mapError(decode(value), (error) => error.message))
135}
136
137/**
138 * Creates a primitive that parses boolean values from string input.
139 *
140 * **Details**
141 *
142 * Recognizes various forms of true/false values:
143 * - True values: "true", "1", "y", "yes", "on"
144 * - False values: "false", "0", "n", "no", "off"
145 *
146 * **Example** (Parsing boolean values)
147 *
148 * ```ts import.meta.vitest
149 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
150 * import { Primitive } from "effect/cli"
151 * import { ChildProcessSpawner } from "effect/process"
152 *
153 * const CliTestLayer = Layer.mergeAll(
154 * FileSystem.layerNoop({}),
155 * Path.layer,
156 * Stdio.layerTest({}),
157 * Layer.succeed(Terminal.Terminal, Terminal.make({
158 * columns: Effect.succeed(80),
159 * rows: Effect.succeed(24),
160 * readInput: Effect.die("unused"),
161 * readLine: Effect.die("unused"),
162 * display: () => Effect.void
163 * })),
164 * Layer.succeed(
165 * ChildProcessSpawner.ChildProcessSpawner,
166 * ChildProcessSpawner.make(() => Effect.die("unused"))
167 * )
168 * )
169 *
170 * const parseBoolean = Effect.all([
171 * Primitive.Boolean.parse("true"),
172 * Primitive.Boolean.parse("yes"),
173 * Primitive.Boolean.parse("false"),
174 * Primitive.Boolean.parse("0")
175 * ])
176 *
177 * await Effect.runPromise(parseBoolean.pipe(Effect.provide(CliTestLayer))) // => [true, true, false, false]
178 * ```
179 *
180 * @stability unstable
181 * @category constructors
182 * @since 4.0.0
183 */
184export const Boolean: Primitive<boolean> = makeSchemaPrimitive(
185 "Boolean",
186 Schema.BooleanLiterals
187)
188
189/**
190 * Creates a primitive that parses finite numbers from string input.
191 *
192 * **Example** (Parsing finite numbers)
193 *
194 * ```ts import.meta.vitest
195 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
196 * import { Primitive } from "effect/cli"
197 * import { ChildProcessSpawner } from "effect/process"
198 *
199 * const CliTestLayer = Layer.mergeAll(
200 * FileSystem.layerNoop({}),
201 * Path.layer,
202 * Stdio.layerTest({}),
203 * Layer.succeed(Terminal.Terminal, Terminal.make({
204 * columns: Effect.succeed(80),
205 * rows: Effect.succeed(24),
206 * readInput: Effect.die("unused"),
207 * readLine: Effect.die("unused"),
208 * display: () => Effect.void
209 * })),
210 * Layer.succeed(
211 * ChildProcessSpawner.ChildProcessSpawner,
212 * ChildProcessSpawner.make(() => Effect.die("unused"))
213 * )
214 * )
215 *
216 * const parseFloat = Effect.all([
217 * Primitive.Finite.parse("3.14"),
218 * Primitive.Finite.parse("-42.5"),
219 * Primitive.Finite.parse("0")
220 * ])
221 *
222 * await Effect.runPromise(parseFloat.pipe(Effect.provide(CliTestLayer))) // => [3.14, -42.5, 0]
223 * ```
224 *
225 * @stability unstable
226 * @category constructors
227 * @since 4.0.0
228 */
229export const Finite: Primitive<number> = makeSchemaPrimitive(
230 "Finite",
231 Schema.Finite
232)
233
234/**
235 * Creates a primitive that parses integer numbers from string input.
236 *
237 * **Example** (Parsing integer values)
238 *
239 * ```ts import.meta.vitest
240 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
241 * import { Primitive } from "effect/cli"
242 * import { ChildProcessSpawner } from "effect/process"
243 *
244 * const CliTestLayer = Layer.mergeAll(
245 * FileSystem.layerNoop({}),
246 * Path.layer,
247 * Stdio.layerTest({}),
248 * Layer.succeed(Terminal.Terminal, Terminal.make({
249 * columns: Effect.succeed(80),
250 * rows: Effect.succeed(24),
251 * readInput: Effect.die("unused"),
252 * readLine: Effect.die("unused"),
253 * display: () => Effect.void
254 * })),
255 * Layer.succeed(
256 * ChildProcessSpawner.ChildProcessSpawner,
257 * ChildProcessSpawner.make(() => Effect.die("unused"))
258 * )
259 * )
260 *
261 * const parseInteger = Effect.all([
262 * Primitive.Int.parse("42"),
263 * Primitive.Int.parse("-123"),
264 * Primitive.Int.parse("0")
265 * ])
266 *
267 * await Effect.runPromise(parseInteger.pipe(Effect.provide(CliTestLayer))) // => [42, -123, 0]
268 * ```
269 *
270 * @stability unstable
271 * @category constructors
272 * @since 4.0.0
273 */
274export const Int: Primitive<number> = makeSchemaPrimitive(
275 "Int",
276 Schema.Int
277)
278
279/**
280 * Creates a primitive that parses Date objects from string input.
281 *
282 * **Example** (Parsing date values)
283 *
284 * ```ts import.meta.vitest
285 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
286 * import { Primitive } from "effect/cli"
287 * import { ChildProcessSpawner } from "effect/process"
288 *
289 * const CliTestLayer = Layer.mergeAll(
290 * FileSystem.layerNoop({}),
291 * Path.layer,
292 * Stdio.layerTest({}),
293 * Layer.succeed(Terminal.Terminal, Terminal.make({
294 * columns: Effect.succeed(80),
295 * rows: Effect.succeed(24),
296 * readInput: Effect.die("unused"),
297 * readLine: Effect.die("unused"),
298 * display: () => Effect.void
299 * })),
300 * Layer.succeed(
301 * ChildProcessSpawner.ChildProcessSpawner,
302 * ChildProcessSpawner.make(() => Effect.die("unused"))
303 * )
304 * )
305 *
306 * const parseDate = Effect.gen(function*() {
307 * const result = yield* Primitive.Date.parse("2023-12-25")
308 * return result.toISOString()
309 * })
310 *
311 * await Effect.runPromise(parseDate.pipe(Effect.provide(CliTestLayer))) // => "2023-12-25T00:00:00.000Z"
312 * ```
313 *
314 * @stability unstable
315 * @category constructors
316 * @since 4.0.0
317 */
318export const Date: Primitive<globalThis.Date> = makeSchemaPrimitive(
319 "Date",
320 Schema.Date
321)
322
323/**
324 * Creates a primitive that accepts any string value without validation.
325 *
326 * **Example** (Parsing string values)
327 *
328 * ```ts import.meta.vitest
329 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
330 * import { Primitive } from "effect/cli"
331 * import { ChildProcessSpawner } from "effect/process"
332 *
333 * const CliTestLayer = Layer.mergeAll(
334 * FileSystem.layerNoop({}),
335 * Path.layer,
336 * Stdio.layerTest({}),
337 * Layer.succeed(Terminal.Terminal, Terminal.make({
338 * columns: Effect.succeed(80),
339 * rows: Effect.succeed(24),
340 * readInput: Effect.die("unused"),
341 * readLine: Effect.die("unused"),
342 * display: () => Effect.void
343 * })),
344 * Layer.succeed(
345 * ChildProcessSpawner.ChildProcessSpawner,
346 * ChildProcessSpawner.make(() => Effect.die("unused"))
347 * )
348 * )
349 *
350 * const parseString = Effect.all([
351 * Primitive.String.parse("hello world"),
352 * Primitive.String.parse(""),
353 * Primitive.String.parse("123")
354 * ])
355 *
356 * await Effect.runPromise(parseString.pipe(Effect.provide(CliTestLayer))) // => ["hello world", "", "123"]
357 * ```
358 *
359 * @stability unstable
360 * @category constructors
361 * @since 4.0.0
362 */
363export const String: Primitive<string> = makePrimitive("String", (value) => Effect.succeed(value))
364
365/**
366 * Creates a primitive that accepts only specific choice values mapped to custom types.
367 *
368 * **Example** (Parsing choices)
369 *
370 * ```ts import.meta.vitest
371 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
372 * import { Primitive } from "effect/cli"
373 * import { ChildProcessSpawner } from "effect/process"
374 *
375 * const CliTestLayer = Layer.mergeAll(
376 * FileSystem.layerNoop({}),
377 * Path.layer,
378 * Stdio.layerTest({}),
379 * Layer.succeed(Terminal.Terminal, Terminal.make({
380 * columns: Effect.succeed(80),
381 * rows: Effect.succeed(24),
382 * readInput: Effect.die("unused"),
383 * readLine: Effect.die("unused"),
384 * display: () => Effect.void
385 * })),
386 * Layer.succeed(
387 * ChildProcessSpawner.ChildProcessSpawner,
388 * ChildProcessSpawner.make(() => Effect.die("unused"))
389 * )
390 * )
391 *
392 * type LogLevel = "debug" | "info" | "warn" | "error"
393 *
394 * const logLevelPrimitive = Primitive.Choice<LogLevel>([
395 * ["debug", "debug"],
396 * ["info", "info"],
397 * ["warn", "warn"],
398 * ["error", "error"]
399 * ])
400 *
401 * const parseLogLevel = Effect.all([
402 * logLevelPrimitive.parse("info"),
403 * logLevelPrimitive.parse("debug")
404 * ])
405 *
406 * await Effect.runPromise(parseLogLevel.pipe(Effect.provide(CliTestLayer))) // => ["info", "debug"]
407 * ```
408 *
409 * @stability unstable
410 * @category constructors
411 * @since 4.0.0
412 */
413export const Choice = <A>(
414 choices: ReadonlyArray<readonly [string, A]>
415): Primitive<A> => {
416 const choiceMap = new Map(choices)
417 const validChoices = choices.map(([key]) => format(key)).join(" | ")
418 const primitive = makePrimitive("Choice", (value) => {
419 if (choiceMap.has(value)) {
420 return Effect.succeed(choiceMap.get(value)!)
421 }
422 return Effect.fail(validChoices)
423 })
424 return Object.assign(primitive, { choiceKeys: choices.map(([key]) => key) })
425}
426
427/**
428 * Specifies the type of path validation to perform.
429 *
430 * **Example** (Choosing path validation)
431 *
432 * ```ts import.meta.vitest
433 * import { Primitive } from "effect/cli"
434 *
435 * // Only accept files
436 * const filePath = Primitive.Path("file", true)
437 *
438 * // Only accept directories
439 * const dirPath = Primitive.Path("directory", true)
440 *
441 * // Accept either files or directories
442 * const anyPath = Primitive.Path("either", false)
443 *
444 * const tags = [filePath._tag, dirPath._tag, anyPath._tag] // => ["Path", "Path", "Path"]
445 * ```
446 *
447 * @stability unstable
448 * @category models
449 * @since 4.0.0
450 */
451export type PathType = "file" | "directory" | "either"
452
453/**
454 * Creates a primitive that validates and resolves file system paths.
455 *
456 * **Example** (Parsing file system paths)
457 *
458 * ```ts import.meta.vitest
459 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
460 * import { Primitive } from "effect/cli"
461 * import { ChildProcessSpawner } from "effect/process"
462 *
463 * const services = Layer.mergeAll(
464 * Path.layer,
465 * FileSystem.layerNoop({
466 * exists: () => Effect.succeed(true),
467 * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info)
468 * }),
469 * Stdio.layerTest({}),
470 * Layer.succeed(Terminal.Terminal, Terminal.make({
471 * columns: Effect.succeed(80),
472 * rows: Effect.succeed(24),
473 * readInput: Effect.die("unused"),
474 * readLine: Effect.die("unused"),
475 * display: () => Effect.void
476 * })),
477 * Layer.succeed(
478 * ChildProcessSpawner.ChildProcessSpawner,
479 * ChildProcessSpawner.make(() => Effect.die("unused"))
480 * )
481 * )
482 *
483 * const program = Effect.gen(function*() {
484 * const filePrimitive = Primitive.Path("file", true)
485 * const filePath = yield* filePrimitive.parse("./package.json")
486 * return filePath.endsWith("/package.json")
487 * }).pipe(Effect.provide(services))
488 *
489 * await Effect.runPromise(program) // => true
490 * ```
491 *
492 * @stability unstable
493 * @category constructors
494 * @since 4.0.0
495 */
496export const Path = (
497 pathType: PathType,
498 mustExist?: boolean
499): Primitive<string> => {
500 const primitive = makePrimitive(
501 "Path",
502 Effect.fnUntraced(function*(value) {
503 const fs = yield* FileSystem.FileSystem
504 const path = yield* Path_.Path
505
506 // Resolve the path to absolute
507 const absolutePath = path.isAbsolute(value) ? value : path.resolve(value)
508
509 // Check if path exists
510 const exists = yield* Effect.mapError(
511 fs.exists(absolutePath),
512 (error) => `Failed to check path existence: ${error.message}`
513 )
514
515 // Validate existence requirements
516 if (mustExist === true && !exists) {
517 return yield* Effect.fail(`Path does not exist: ${absolutePath}`)
518 }
519
520 // Validate path type if it exists
521 if (exists && pathType !== "either") {
522 const stat = yield* Effect.mapError(
523 fs.stat(absolutePath),
524 (error) => `Failed to stat path: ${error.message}`
525 )
526
527 if (pathType === "file" && stat.type !== "File") {
528 return yield* Effect.fail(`Path is not a file: ${absolutePath}`)
529 }
530 if (pathType === "directory" && stat.type !== "Directory") {
531 return yield* Effect.fail(`Path is not a directory: ${absolutePath}`)
532 }
533 }
534
535 return absolutePath
536 })
537 )
538 return Object.assign(primitive, { pathType })
539}
540
541/**
542 * Creates a primitive that wraps string input in `Redacted`.
543 *
544 * **Details**
545 *
546 * The wrapped value is hidden when formatted or inspected, while the original
547 * string remains available through the `Redacted` API when explicitly needed.
548 *
549 * **Example** (Parsing redacted values)
550 *
551 * ```ts import.meta.vitest
552 * import { Effect, FileSystem, Layer, Path, Redacted, Stdio, Terminal } from "effect"
553 * import { Primitive } from "effect/cli"
554 * import { ChildProcessSpawner } from "effect/process"
555 *
556 * const CliTestLayer = Layer.mergeAll(
557 * FileSystem.layerNoop({}),
558 * Path.layer,
559 * Stdio.layerTest({}),
560 * Layer.succeed(Terminal.Terminal, Terminal.make({
561 * columns: Effect.succeed(80),
562 * rows: Effect.succeed(24),
563 * readInput: Effect.die("unused"),
564 * readLine: Effect.die("unused"),
565 * display: () => Effect.void
566 * })),
567 * Layer.succeed(
568 * ChildProcessSpawner.ChildProcessSpawner,
569 * ChildProcessSpawner.make(() => Effect.die("unused"))
570 * )
571 * )
572 *
573 * const parseRedacted = Effect.gen(function*() {
574 * const result = yield* Primitive.Redacted.parse("secret-password")
575 * return [Redacted.value(result), String(result)] as const
576 * })
577 *
578 * await Effect.runPromise(parseRedacted.pipe(Effect.provide(CliTestLayer))) // => ["secret-password", "<redacted>"]
579 * ```
580 *
581 * @stability unstable
582 * @category constructors
583 * @since 4.0.0
584 */
585export const Redacted: Primitive<Redacted_.Redacted<string>> = makePrimitive(
586 "Redacted",
587 (value) => Effect.succeed(Redacted_.make(value))
588)
589
590/**
591 * Creates a primitive that reads and returns the contents of a file as a string.
592 *
593 * **Example** (Reading file text)
594 *
595 * ```ts import.meta.vitest
596 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
597 * import { Primitive } from "effect/cli"
598 * import { ChildProcessSpawner } from "effect/process"
599 *
600 * const services = Layer.mergeAll(
601 * Path.layer,
602 * FileSystem.layerNoop({
603 * exists: () => Effect.succeed(true),
604 * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info),
605 * readFileString: () => Effect.succeed('{"private":true}')
606 * }),
607 * Stdio.layerTest({}),
608 * Layer.succeed(Terminal.Terminal, Terminal.make({
609 * columns: Effect.succeed(80),
610 * rows: Effect.succeed(24),
611 * readInput: Effect.die("unused"),
612 * readLine: Effect.die("unused"),
613 * display: () => Effect.void
614 * })),
615 * Layer.succeed(
616 * ChildProcessSpawner.ChildProcessSpawner,
617 * ChildProcessSpawner.make(() => Effect.die("unused"))
618 * )
619 * )
620 *
621 * const readConfigFile = Effect.gen(function*() {
622 * const content = yield* Primitive.FileText.parse("./package.json")
623 * return JSON.parse(content) as { private: boolean }
624 * }).pipe(Effect.provide(services))
625 *
626 * await Effect.runPromise(readConfigFile) // => { private: true }
627 * ```
628 *
629 * @stability unstable
630 * @category constructors
631 * @since 4.0.0
632 */
633export const FileText: Primitive<string> = makePrimitive(
634 "FileText",
635 Effect.fnUntraced(function*(filePath) {
636 const fs = yield* FileSystem.FileSystem
637 const path = yield* Path_.Path
638
639 // Resolve to absolute path
640 const absolutePath = path.isAbsolute(filePath)
641 ? filePath
642 : path.resolve(filePath)
643
644 // Check if file exists
645 const exists = yield* Effect.mapError(
646 fs.exists(absolutePath),
647 (error) => `Failed to check file existence: ${error.message}`
648 )
649
650 if (!exists) {
651 return yield* Effect.fail(`File does not exist: ${absolutePath}`)
652 }
653
654 // Check if it's actually a file
655 const stat = yield* Effect.mapError(
656 fs.stat(absolutePath),
657 (error) => `Failed to stat file: ${error.message}`
658 )
659
660 if (stat.type !== "File") {
661 return yield* Effect.fail(`Path is not a file: ${absolutePath}`)
662 }
663
664 // Read file content
665 const content = yield* Effect.mapError(
666 fs.readFileString(absolutePath),
667 (error) => `Failed to read file: ${error.message}`
668 )
669
670 return content
671 })
672)
673
674/**
675 * Represents options which can be provided to methods that deal with parsing
676 * file content.
677 *
678 * @stability unstable
679 * @category options
680 * @since 4.0.0
681 */
682export type FileParseOptions = {
683 readonly format?: "ini" | "json" | "toml" | "yaml"
684}
685
686const fileParsers: Record<string, (content: string) => unknown> = {
687 ini: (content: string) => Ini.parse(content),
688 json: (content: string) => JSON.parse(content),
689 toml: (content: string) => Toml.parse(content),
690 yml: (content: string) => Yaml.parse(content),
691 yaml: (content: string) => Yaml.parse(content)
692}
693
694/**
695 * Creates a primitive that reads a file and parses its content as structured
696 * data.
697 *
698 * **Details**
699 *
700 * The parser is selected from `options.format` when provided, otherwise from
701 * the file extension. Supported formats include INI, JSON, TOML, YAML, and YML.
702 *
703 * **Example** (Parsing file content)
704 *
705 * ```ts import.meta.vitest
706 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
707 * import { Primitive } from "effect/cli"
708 * import { ChildProcessSpawner } from "effect/process"
709 *
710 * const services = Layer.mergeAll(
711 * Path.layer,
712 * FileSystem.layerNoop({
713 * exists: () => Effect.succeed(true),
714 * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info),
715 * readFileString: () => Effect.succeed('{"private":true}')
716 * }),
717 * Stdio.layerTest({}),
718 * Layer.succeed(Terminal.Terminal, Terminal.make({
719 * columns: Effect.succeed(80),
720 * rows: Effect.succeed(24),
721 * readInput: Effect.die("unused"),
722 * readLine: Effect.die("unused"),
723 * display: () => Effect.void
724 * })),
725 * Layer.succeed(
726 * ChildProcessSpawner.ChildProcessSpawner,
727 * ChildProcessSpawner.make(() => Effect.die("unused"))
728 * )
729 * )
730 *
731 * const jsonFilePrimitive = Primitive.FileParse({ format: "json" })
732 *
733 * const loadConfig = Effect.gen(function*() {
734 * const config = yield* jsonFilePrimitive.parse("./package.json")
735 * return config as { private: boolean }
736 * }).pipe(Effect.provide(services))
737 *
738 * await Effect.runPromise(loadConfig) // => { private: true }
739 * ```
740 *
741 * @stability unstable
742 * @category constructors
743 * @since 4.0.0
744 */
745export const FileParse = (options?: FileParseOptions): Primitive<unknown> => {
746 return makePrimitive(
747 "FileParse",
748 Effect.fnUntraced(function*(filePath) {
749 const fileFormat = options?.format ?? filePath.split(".").pop() as string
750 const parser = fileParsers[fileFormat]
751 if (parser === undefined) {
752 return yield* Effect.fail(`Unsupported file format: ${fileFormat}`)
753 }
754 const content = yield* FileText.parse(filePath)
755 return yield* Effect.try({
756 try: () => parser(content),
757 catch: (error) => `Failed to parse '.${fileFormat}' file content: ${error}`
758 })
759 })
760 )
761}
762
763/**
764 * Represents options which can be provided to methods that deal with parsing
765 * file content and decoding the file content with a `Schema`.
766 *
767 * @stability unstable
768 * @category options
769 * @since 4.0.0
770 */
771export type FileSchemaOptions = Struct.Simplify<
772 FileParseOptions & {
773 readonly errorFormatter?: Formatter<string> | undefined
774 }
775>
776
777/**
778 * Reads and parses file content using the specified schema.
779 *
780 * **Example** (Parsing file content with a schema)
781 *
782 * ```ts import.meta.vitest
783 * import { Effect, FileSystem, Layer, Path, Schema, Stdio, Terminal } from "effect"
784 * import { Primitive } from "effect/cli"
785 * import { ChildProcessSpawner } from "effect/process"
786 *
787 * const services = Layer.mergeAll(
788 * Path.layer,
789 * FileSystem.layerNoop({
790 * exists: () => Effect.succeed(true),
791 * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info),
792 * readFileString: () => Effect.succeed('{"private":true}')
793 * }),
794 * Stdio.layerTest({}),
795 * Layer.succeed(Terminal.Terminal, Terminal.make({
796 * columns: Effect.succeed(80),
797 * rows: Effect.succeed(24),
798 * readInput: Effect.die("unused"),
799 * readLine: Effect.die("unused"),
800 * display: () => Effect.void
801 * })),
802 * Layer.succeed(
803 * ChildProcessSpawner.ChildProcessSpawner,
804 * ChildProcessSpawner.make(() => Effect.die("unused"))
805 * )
806 * )
807 *
808 * const ConfigSchema = Schema.Struct({
809 * private: Schema.Boolean
810 * })
811 *
812 * const jsonConfigPrimitive = Primitive.FileSchema(ConfigSchema, {
813 * format: "json"
814 * })
815 *
816 * const loadConfig = Effect.gen(function*() {
817 * return yield* jsonConfigPrimitive.parse("./package.json")
818 * }).pipe(Effect.provide(services))
819 *
820 * await Effect.runPromise(loadConfig) // => { private: true }
821 * ```
822 *
823 * @stability unstable
824 * @category constructors
825 * @since 4.0.0
826 */
827export const FileSchema = <A>(
828 schema: Schema.ConstraintDecoder<A, Environment>,
829 options?: FileSchemaOptions | undefined
830): Primitive<A> => {
831 const decode = Schema.decodeUnknownEffect(schema)
832 return makePrimitive(
833 "FileSchema",
834 Effect.fnUntraced(function*(filePath) {
835 const content = yield* FileParse(options).parse(filePath)
836 return yield* Effect.mapError(
837 decode(content),
838 (error) => options?.errorFormatter?.(error.issue) ?? error.toString()
839 )
840 })
841 )
842}
843
844/**
845 * Parses a single `key=value` pair into a record object.
846 *
847 * **Details**
848 *
849 * Splits at the first `=`. Keys and values must be non-empty; values may contain `=`.
850 *
851 * **Example** (Parsing key-value pairs)
852 *
853 * ```ts import.meta.vitest
854 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
855 * import { Primitive } from "effect/cli"
856 * import { ChildProcessSpawner } from "effect/process"
857 *
858 * const CliTestLayer = Layer.mergeAll(
859 * FileSystem.layerNoop({}),
860 * Path.layer,
861 * Stdio.layerTest({}),
862 * Layer.succeed(Terminal.Terminal, Terminal.make({
863 * columns: Effect.succeed(80),
864 * rows: Effect.succeed(24),
865 * readInput: Effect.die("unused"),
866 * readLine: Effect.die("unused"),
867 * display: () => Effect.void
868 * })),
869 * Layer.succeed(
870 * ChildProcessSpawner.ChildProcessSpawner,
871 * ChildProcessSpawner.make(() => Effect.die("unused"))
872 * )
873 * )
874 *
875 * const parseKeyValue = Effect.all([
876 * Primitive.KeyValuePair.parse("name=john"),
877 * Primitive.KeyValuePair.parse("port=3000"),
878 * Primitive.KeyValuePair.parse("debug=true")
879 * ])
880 *
881 * const result = await Effect.runPromise(parseKeyValue.pipe(Effect.provide(CliTestLayer)))
882 * result // => [{ name: "john" }, { port: "3000" }, { debug: "true" }]
883 * ```
884 *
885 * @stability unstable
886 * @category constructors
887 * @since 4.0.0
888 */
889export const KeyValuePair: Primitive<Record<string, string>> = makePrimitive(
890 "KeyValuePair",
891 Effect.fnUntraced(function*(value) {
892 const separator = value.indexOf("=")
893 if (separator === -1) {
894 return yield* Effect.fail(
895 `Invalid key=value format. Expected format: key=value, got: ${value}`
896 )
897 }
898 const key = value.slice(0, separator)
899 const val = value.slice(separator + 1)
900 if (!key || !val) {
901 return yield* Effect.fail(
902 `Invalid key=value format. Both key and value must be non-empty. Got: ${value}`
903 )
904 }
905 return { [key]: val }
906 })
907)
908
909/**
910 * A primitive that always fails to parse.
911 *
912 * **Example** (Rejecting option values)
913 *
914 * ```ts import.meta.vitest
915 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
916 * import { Primitive } from "effect/cli"
917 * import { ChildProcessSpawner } from "effect/process"
918 *
919 * const CliTestLayer = Layer.mergeAll(
920 * FileSystem.layerNoop({}),
921 * Path.layer,
922 * Stdio.layerTest({}),
923 * Layer.succeed(Terminal.Terminal, Terminal.make({
924 * columns: Effect.succeed(80),
925 * rows: Effect.succeed(24),
926 * readInput: Effect.die("unused"),
927 * readLine: Effect.die("unused"),
928 * display: () => Effect.void
929 * })),
930 * Layer.succeed(
931 * ChildProcessSpawner.ChildProcessSpawner,
932 * ChildProcessSpawner.make(() => Effect.die("unused"))
933 * )
934 * )
935 *
936 * const program = Effect.gen(function*() {
937 * return yield* Primitive.Never.parse("any-value")
938 * })
939 *
940 * await Effect.runPromise(Effect.flip(program).pipe(Effect.provide(CliTestLayer))) // => "This option does not accept values"
941 * ```
942 *
943 * @stability unstable
944 * @category constructors
945 * @since 4.0.0
946 */
947export const Never: Primitive<never> = makePrimitive("Never", () => Effect.fail("This option does not accept values"))
948
949/**
950 * Gets a human-readable type name for a primitive.
951 *
952 * **When to use**
953 *
954 * Use when you need the display type name for a `Primitive`, such as when
955 * generating CLI help documentation.
956 *
957 * **Example** (Getting primitive type names)
958 *
959 * ```ts import.meta.vitest
960 * import { Primitive } from "effect/cli"
961 *
962 * Primitive.getTypeName(Primitive.String) // => "string"
963 * Primitive.getTypeName(Primitive.Int) // => "integer"
964 * Primitive.getTypeName(Primitive.Boolean) // => "boolean"
965 * Primitive.getTypeName(Primitive.Date) // => "date"
966 * Primitive.getTypeName(Primitive.KeyValuePair) // => "key=value"
967 *
968 * const logLevelChoice = Primitive.Choice([
969 * ["debug", "debug"],
970 * ["info", "info"]
971 * ])
972 * Primitive.getTypeName(logLevelChoice) // => "choice"
973 * ```
974 *
975 * @stability unstable
976 * @category getters
977 * @since 4.0.0
978 */
979export const getTypeName = <A>(primitive: Primitive<A>): string => {
980 switch (primitive._tag) {
981 case "Boolean":
982 return "boolean"
983 case "String":
984 return "string"
985 case "Int":
986 return "integer"
987 case "Finite":
988 return "number"
989 case "Date":
990 return "date"
991 case "Path":
992 return "path"
993 case "Choice":
994 return "choice"
995 case "Redacted":
996 return "string"
997 case "FileText":
998 return "file"
999 case "FileParse":
1000 return "file"
1001 case "FileSchema":
1002 return "file"
1003 case "KeyValuePair":
1004 return "key=value"
1005 case "Never":
1006 return "none"
1007 default:
1008 return "value"
1009 }
1010}
1011
1012/**
1013 * @internal
1014 */
1015export const getChoiceKeys = (primitive: Primitive<unknown>): ReadonlyArray<string> | undefined =>
1016 primitive._tag === "Choice" ? (primitive as any).choiceKeys : undefined
1017
1018/**
1019 * @internal
1020 */
1021export const getPathType = (primitive: Primitive<unknown>): PathType | undefined =>
1022 primitive._tag === "Path" ? (primitive as any).pathType : undefined