| 1 | import * as Data from "effect/Data"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Runtime from "effect/Runtime"; |
| 4 | import * as CliKit from "../CliKit/index.ts"; |
| 5 | import { exitDeclined } from "./errors.ts"; |
| 6 | |
| 7 | /** |
| 8 | * A declined confirmation prompt. By the time this propagates the decline |
| 9 | * message has already been printed and the exit code set, so the |
| 10 | * `[Runtime.errorReported] = false` marker suppresses the runtime's default |
| 11 | * failure log — the process just exits 1, matching the CLI's documented |
| 12 | * "1 = failure or decline" contract. |
| 13 | */ |
| 14 | export class ConfirmationDeclined extends Data.TaggedError( |
| 15 | "ConfirmationDeclined", |
| 16 | ) { |
| 17 | readonly [Runtime.errorReported] = false; |
| 18 | } |
| 19 | |
| 20 | /** |
| 21 | * Gate a destructive operation behind a confirmation prompt. |
| 22 | * |
| 23 | * Returns void when `yes` was passed or the user approves. Otherwise prints |
| 24 | * `abortMessage` (default "Aborted."), sets the exit code to 1 via |
| 25 | * {@link exitDeclined}, and fails with {@link ConfirmationDeclined} so the |
| 26 | * command short-circuits before the destructive work. |
| 27 | */ |
| 28 | export const confirmOrDecline = Effect.fn(function* (options: { |
| 29 | readonly yes: boolean; |
| 30 | readonly message: string; |
| 31 | readonly confirmLabel?: string; |
| 32 | readonly cancelLabel?: string; |
| 33 | readonly abortMessage?: string; |
| 34 | }) { |
| 35 | if (options.yes) return; |
| 36 | const approved = yield* CliKit.accessors.prompt.confirm({ |
| 37 | message: options.message, |
| 38 | initialValue: false, |
| 39 | confirmLabel: options.confirmLabel, |
| 40 | cancelLabel: options.cancelLabel, |
| 41 | }); |
| 42 | if (approved) return; |
| 43 | yield* CliKit.accessors.output.info(options.abortMessage ?? "Aborted."); |
| 44 | yield* exitDeclined; |
| 45 | return yield* Effect.fail(new ConfirmationDeclined()); |
| 46 | }); |