From 33c90d7634863ad9da02a49e277e5a40ace430af Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Thu, 16 Nov 2023 11:41:48 -0500 Subject: [PATCH] implement wizard and completions --- .changeset/calm-doors-do.md | 5 + .changeset/clever-waves-tie.md | 5 + .gitignore | 1 + .prettierignore | 2 - examples/git.ts | 44 +- examples/prompt.ts | 61 +- flake.lock | 6 +- package.json | 14 +- pnpm-lock.yaml | 1051 +++++++++++++++++++++-------- src/Args.ts | 14 +- src/BuiltInOptions.ts | 4 +- src/CliApp.ts | 3 +- src/Command.ts | 13 +- src/Compgen.ts | 6 +- src/Options.ts | 26 +- src/Parameter.ts | 47 -- src/Primitive.ts | 9 +- src/Prompt.ts | 316 ++++++++- src/Prompt/Action.ts | 27 +- src/Terminal.ts | 76 --- src/index.ts | 10 - src/internal/args.ts | 239 ++++--- src/internal/builtInOptions.ts | 4 +- src/internal/cliApp.ts | 160 +++-- src/internal/command.ts | 205 ++++-- src/internal/compgen.ts | 6 +- src/internal/helpDoc.ts | 80 ++- src/internal/options.ts | 360 ++++++---- src/internal/primitive.ts | 130 +++- src/internal/prompt.ts | 94 +-- src/internal/prompt/action.ts | 6 - src/internal/prompt/ansi-utils.ts | 217 +++--- src/internal/prompt/confirm.ts | 164 +++++ src/internal/prompt/date.ts | 636 +++++++++++++++++ src/internal/prompt/list.ts | 9 + src/internal/prompt/number.ts | 348 ++++++---- src/internal/prompt/select.ts | 272 +++++--- src/internal/prompt/text.ts | 285 +++++--- src/internal/prompt/toggle.ts | 171 +++++ src/internal/prompt/utils.ts | 4 +- src/internal/terminal.ts | 129 ---- test/Command.test.ts | 10 +- test/Completion.test.ts | 22 +- test/Primitive.test.ts | 2 +- 44 files changed, 3734 insertions(+), 1559 deletions(-) create mode 100644 .changeset/calm-doors-do.md create mode 100644 .changeset/clever-waves-tie.md delete mode 100644 .prettierignore delete mode 100644 src/Parameter.ts delete mode 100644 src/Terminal.ts create mode 100644 src/internal/prompt/confirm.ts create mode 100644 src/internal/prompt/date.ts create mode 100644 src/internal/prompt/list.ts create mode 100644 src/internal/prompt/toggle.ts delete mode 100644 src/internal/terminal.ts diff --git a/.changeset/calm-doors-do.md b/.changeset/calm-doors-do.md new file mode 100644 index 0000000..34243f2 --- /dev/null +++ b/.changeset/calm-doors-do.md @@ -0,0 +1,5 @@ +--- +"@effect/cli": minor +--- + +implement `--wizard` mode for cli applications diff --git a/.changeset/clever-waves-tie.md b/.changeset/clever-waves-tie.md new file mode 100644 index 0000000..6a96c8b --- /dev/null +++ b/.changeset/clever-waves-tie.md @@ -0,0 +1,5 @@ +--- +"@effect/cli": minor +--- + +implement completion script generation for cli applications diff --git a/.gitignore b/.gitignore index c3af0f5..662a338 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tmp/ build/ dist/ .direnv/ +docs/ diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index afe8be7..0000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -*.js -*.ts diff --git a/examples/git.ts b/examples/git.ts index 330c914..ee5dbc7 100644 --- a/examples/git.ts +++ b/examples/git.ts @@ -5,9 +5,10 @@ import * as HelpDoc from "@effect/cli/HelpDoc" import * as Span from "@effect/cli/HelpDoc/Span" import * as Options from "@effect/cli/Options" import * as NodeContext from "@effect/platform-node/NodeContext" +import * as Terminal from "@effect/platform-node/Terminal" import * as Data from "effect/Data" import * as Effect from "effect/Effect" -import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" import * as Option from "effect/Option" export interface Git extends Data.Case { @@ -52,41 +53,42 @@ export interface RemoveRemote extends Data.Case { export const RemoveRemote = Data.tagged("RemoveRemote") -const add: Command.Command = pipe( - Command.standard("add", { - options: Options.boolean("m"), - args: Args.text({ name: "directory" }) - }), +const add: Command.Command = Command.standard("add", { + options: Options.boolean("m"), + args: Args.text({ name: "directory" }) +}).pipe( Command.withHelp(HelpDoc.p("Description of the `git add` subcommand")), Command.map(({ args: directory, options: modified }) => Add({ modified, directory })) ) -const addRemote: Command.Command = pipe( - Command.standard("add", { - options: Options.all({ - name: Options.text("name"), - url: Options.text("url") - }) - }), +const addRemote: Command.Command = Command.standard("add", { + options: Options.all({ + name: Options.text("name"), + url: Options.text("url") + }) +}).pipe( Command.withHelp(HelpDoc.p("Description of the `git remote add` subcommand")), Command.map(({ options: { name, url } }) => AddRemote({ name, url })) ) -const removeRemote: Command.Command = pipe( - Command.standard("remove", { args: Args.text({ name: "name" }) }), +const removeRemote: Command.Command = Command.standard("remove", { + args: Args.text({ name: "name" }) +}).pipe( Command.withHelp(HelpDoc.p("Description of the `git remote remove` subcommand")), Command.map(({ args: name }) => RemoveRemote({ name })) ) -const remote: Command.Command = pipe( - Command.standard("remote", { options: Options.boolean("verbose").pipe(Options.withAlias("v")) }), +const remote: Command.Command = Command.standard("remote", { + options: Options.boolean("verbose").pipe(Options.withAlias("v")) +}).pipe( Command.withHelp("Description of the `git remote` subcommand"), Command.subcommands([addRemote, removeRemote]), Command.map(({ options: verbose, subcommand }) => Remote({ verbose, subcommand })) ) -const git: Command.Command = pipe( - Command.standard("git", { options: Options.boolean("version").pipe(Options.withAlias("v")) }), +const git: Command.Command = Command.standard("git", { + options: Options.boolean("version").pipe(Options.withAlias("v")) +}).pipe( Command.subcommands([add, remote]), Command.map(({ options: version, subcommand }) => Git({ version, subcommand })) ) @@ -132,6 +134,8 @@ const cli = CliApp.make({ footer: HelpDoc.p("Copyright 2023") }) +const MainLive = Layer.merge(NodeContext.layer, Terminal.layer) + Effect.sync(() => process.argv.slice(2)).pipe( Effect.flatMap((args) => CliApp.run(cli, args, (command) => @@ -143,6 +147,6 @@ Effect.sync(() => process.argv.slice(2)).pipe( onSome: handleGitSubcommand })) ), - Effect.provide(NodeContext.layer), + Effect.provide(MainLive), Effect.runFork ) diff --git a/examples/prompt.ts b/examples/prompt.ts index 3560375..2499c51 100644 --- a/examples/prompt.ts +++ b/examples/prompt.ts @@ -2,31 +2,66 @@ import * as CliApp from "@effect/cli/CliApp" import * as Command from "@effect/cli/Command" import * as Prompt from "@effect/cli/Prompt" import * as NodeContext from "@effect/platform-node/NodeContext" +import * as Terminal from "@effect/platform-node/Terminal" import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" -const colorPrompt = Prompt.select({ - message: "Pick your favorite color!", - choices: [ - { title: "Red", value: "#ff0000", description: "This option has a description" }, - { title: "Green", value: "#00ff00" }, - { title: "Blue", value: "#0000ff" } - ] -}) +// const colorPrompt = Prompt.select({ +// message: "Pick your favorite color", +// choices: [ +// { title: "Red", value: "#ff0000", description: "This option has a description" }, +// { title: "Green", value: "#00ff00", description: "So does this one" }, +// { title: "Blue", value: "#0000ff", disabled: true } +// ] +// }) + +// const confirmPrompt = Prompt.confirm({ +// message: "Can you please confirm?" +// }) + +// const datePrompt = Prompt.date({ +// message: "What's your birth day?", +// dateMask: +// "\"Year:\" YYYY, \"Month:\" MM, \"Day:\" DD \\\\\\\\||// \\Hour: HH, \\Minute: mm, \"Seconds:\" ss", +// validate: (date) => +// date.getTime() > Date.now() +// ? Effect.fail("Your birth day can't be in the future") +// : Effect.succeed(date) +// }) -const numberPrompt = Prompt.float({ - message: `What is your favorite number?` +// const numberPrompt = Prompt.float({ +// message: `What is your favorite number?`, +// validate: (n) => n > 0 ? Effect.succeed(n) : Effect.fail("must be greater than 0") +// }) + +// const textPrompt = Prompt.text({ +// message: `Please answer the following question\nWhat is your favorite food?`, +// type: "hidden", +// validate: (value) => +// value.length === 0 +// ? Effect.fail("must be non-empty\nyou entered " + value) +// : Effect.succeed(value) +// }) + +const togglePrompt = Prompt.toggle({ + message: "Can you confirm?", + active: "yes", + inactive: "no" }) -const prompt = Prompt.all([colorPrompt, numberPrompt]) +// const prompt = Prompt.all([colorPrompt, numberPrompt, textPrompt]) const cli = CliApp.make({ name: "Your Favorite Things", version: "0.0.1", - command: Command.prompt("favorites", prompt) + // command: Command.prompt("favorites", prompt) + command: Command.prompt("favorites", togglePrompt) }) +const MainLive = Layer.merge(NodeContext.layer, Terminal.layer) + Effect.sync(() => process.argv.slice(2)).pipe( Effect.flatMap((args) => CliApp.run(cli, args, (input) => Effect.log(input))), - Effect.provide(NodeContext.layer), + Effect.provide(MainLive), Effect.runFork ) diff --git a/flake.lock b/flake.lock index 9dc4b6f..ab13deb 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1699343069, - "narHash": "sha256-s7BBhyLA6MI6FuJgs4F/SgpntHBzz40/qV0xLPW6A1Q=", + "lastModified": 1700108881, + "narHash": "sha256-+Lqybl8kj0+nD/IlAWPPG/RDTa47gff9nbei0u7BntE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ec750fd01963ab6b20ee1f0cb488754e8036d89d", + "rev": "7414e9ee0b3e9903c24d3379f577a417f0aae5f1", "type": "github" }, "original": { diff --git a/package.json b/package.json index 69f119c..a0fc4a3 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "build-esm": "tsc -b tsconfig.build.json", "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", "build-annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", - "clean": "rimraf build dist coverage .tsbuildinfo", + "clean": "rimraf build dist docs coverage .tsbuildinfo", "lint": "eslint src test examples --ext .ts", "lint-fix": "pnpm lint --fix", "check": "tsc -b tsconfig.json", @@ -70,12 +70,12 @@ "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.2", - "@effect/build-utils": "^0.4.1", - "@effect/docgen": "^0.3.2", + "@effect/build-utils": "^0.5.0", + "@effect/docgen": "^0.3.4", "@effect/eslint-plugin": "^0.1.2", "@effect/language-service": "^0.0.21", - "@effect/platform": "^0.30.0", - "@effect/platform-node": "^0.31.0", + "@effect/platform": "^0.30.2", + "@effect/platform-node": "^0.31.2", "@effect/printer": "^0.23.1", "@effect/printer-ansi": "^0.23.1", "@effect/schema": "^0.48.0", @@ -95,11 +95,11 @@ "eslint-plugin-sort-destructure-keys": "^1.5.0", "fast-check": "^3.13.2", "madge": "^6.1.0", - "prettier": "^3.1.0", + "rimraf": "^5.0.5", "stackframe": "^1.3.4", "tsx": "^4.1.2", "typescript": "^5.2.2", - "vite": "^4.5.0", + "vite": "^5.0.0", "vitest": "^0.34.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be80f86..c23b76a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,11 +24,11 @@ devDependencies: specifier: ^2.26.2 version: 2.26.2 '@effect/build-utils': - specifier: ^0.4.1 - version: 0.4.1 + specifier: ^0.5.0 + version: 0.5.0 '@effect/docgen': - specifier: ^0.3.2 - version: 0.3.2(fast-check@3.13.2)(tsx@4.1.2)(typescript@5.2.2) + specifier: ^0.3.4 + version: 0.3.4(tsx@4.1.2)(typescript@5.2.2) '@effect/eslint-plugin': specifier: ^0.1.2 version: 0.1.2 @@ -36,11 +36,11 @@ devDependencies: specifier: ^0.0.21 version: 0.0.21 '@effect/platform': - specifier: ^0.30.0 - version: 0.30.0(@effect/schema@0.48.0)(effect@2.0.0-next.55) + specifier: ^0.30.2 + version: 0.30.2(@effect/schema@0.48.0)(effect@2.0.0-next.55) '@effect/platform-node': - specifier: ^0.31.0 - version: 0.31.0(@effect/schema@0.48.0)(effect@2.0.0-next.55) + specifier: ^0.31.2 + version: 0.31.2(@effect/schema@0.48.0)(effect@2.0.0-next.55) '@effect/printer': specifier: ^0.23.1 version: 0.23.1(@effect/typeclass@0.15.0)(effect@2.0.0-next.55) @@ -98,9 +98,9 @@ devDependencies: madge: specifier: ^6.1.0 version: 6.1.0(typescript@5.2.2) - prettier: - specifier: ^3.1.0 - version: 3.1.0 + rimraf: + specifier: ^5.0.5 + version: 5.0.5 stackframe: specifier: ^1.3.4 version: 1.3.4 @@ -111,8 +111,8 @@ devDependencies: specifier: ^5.2.2 version: 5.2.2 vite: - specifier: ^4.5.0 - version: 4.5.0(@types/node@20.9.0) + specifier: ^5.0.0 + version: 5.0.0(@types/node@20.9.0) vitest: specifier: ^0.34.6 version: 0.34.6 @@ -191,11 +191,21 @@ packages: /@babel/generator@7.12.17: resolution: {integrity: sha512-DSA7ruZrY4WI8VxuS1jWSRezFnghEoYEFrZcw9BizQRmOZiUsiHl59+qEARGPqPikwA/GPTyRCi7isuCK/oyqg==} dependencies: - '@babel/types': 7.23.3 + '@babel/types': 7.23.0 jsesc: 2.5.2 source-map: 0.5.7 dev: true + /@babel/generator@7.23.0: + resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + dev: true + /@babel/generator@7.23.3: resolution: {integrity: sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==} engines: {node: '>=6.9.0'} @@ -227,14 +237,14 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.22.15 - '@babel/types': 7.23.3 + '@babel/types': 7.23.0 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.3 + '@babel/types': 7.23.0 dev: true /@babel/helper-module-imports@7.22.15: @@ -274,7 +284,7 @@ packages: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.3 + '@babel/types': 7.23.0 dev: true /@babel/helper-string-parser@7.22.5: @@ -312,6 +322,14 @@ packages: js-tokens: 4.0.0 dev: true + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.0 + dev: true + /@babel/parser@7.23.3: resolution: {integrity: sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==} engines: {node: '>=6.0.0'} @@ -352,8 +370,8 @@ packages: '@babel/helper-simple-access': 7.22.5 dev: true - /@babel/runtime@7.23.2: - resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} + /@babel/runtime@7.23.1: + resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 @@ -368,6 +386,24 @@ packages: '@babel/types': 7.23.3 dev: true + /@babel/traverse@7.23.2: + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/traverse@7.23.3: resolution: {integrity: sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==} engines: {node: '>=6.9.0'} @@ -386,6 +422,15 @@ packages: - supports-color dev: true + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + /@babel/types@7.23.3: resolution: {integrity: sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==} engines: {node: '>=6.9.0'} @@ -402,7 +447,7 @@ packages: /@changesets/apply-release-plan@6.1.4: resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/config': 2.3.1 '@changesets/get-version-range-type': 0.3.2 '@changesets/git': 2.0.0 @@ -420,7 +465,7 @@ packages: /@changesets/assemble-release-plan@5.2.4: resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/errors': 0.1.4 '@changesets/get-dependents-graph': 1.3.6 '@changesets/types': 5.2.1 @@ -448,7 +493,7 @@ packages: resolution: {integrity: sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==} hasBin: true dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/apply-release-plan': 6.1.4 '@changesets/assemble-release-plan': 5.2.4 '@changesets/changelog-git': 0.1.14 @@ -463,8 +508,8 @@ packages: '@changesets/types': 5.2.1 '@changesets/write': 0.2.3 '@manypkg/get-packages': 1.1.3 - '@types/is-ci': 3.0.4 - '@types/semver': 7.5.5 + '@types/is-ci': 3.0.2 + '@types/semver': 7.5.3 ansi-colors: 4.1.3 chalk: 2.4.2 enquirer: 2.4.1 @@ -480,7 +525,7 @@ packages: semver: 7.5.4 spawndamnit: 2.0.0 term-size: 2.2.1 - tty-table: 4.2.3 + tty-table: 4.2.2 dev: true /@changesets/config@2.3.1: @@ -523,7 +568,7 @@ packages: /@changesets/get-release-plan@3.0.17: resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/assemble-release-plan': 5.2.4 '@changesets/config': 2.3.1 '@changesets/pre': 1.0.14 @@ -539,7 +584,7 @@ packages: /@changesets/git@2.0.0: resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -564,7 +609,7 @@ packages: /@changesets/pre@1.0.14: resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -574,7 +619,7 @@ packages: /@changesets/read@0.5.9: resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/git': 2.0.0 '@changesets/logger': 0.0.5 '@changesets/parse': 0.3.16 @@ -595,7 +640,7 @@ packages: /@changesets/write@0.2.3: resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 @@ -618,34 +663,26 @@ packages: resolution: {integrity: sha512-rPwwm/RrFIolz6xHa8Kzpshuwpe+xu/XcEw9iUmRF2tnyIwxxaW7XoFKaQ+GfPju81cKpH4vJeq7/2IizKvyjg==} dev: true - /@effect/build-utils@0.4.1: - resolution: {integrity: sha512-p4Fm6jAnXEi1HaG6lRQN6uBWfFmMpoXetedMshuKO19EFk9OSAkcTl4Hb2wiE/Ex34xsmxg9gKlFpAZR6gc0ww==} + /@effect/build-utils@0.5.0: + resolution: {integrity: sha512-MnSKSKvIUzcy4/TNS7gHV+DJK5KldMKoQkjgyyq0dKWxxEsfIduNh5fDDjEXABl3xO+2wrSbmjIsE/gOq+nS/A==} engines: {node: '>=16.17.1'} hasBin: true dev: true - /@effect/docgen@0.3.2(fast-check@3.13.2)(tsx@4.1.2)(typescript@5.2.2): - resolution: {integrity: sha512-2Iq0p+Qp+hzHi9BGKZ7dbZ35c+RZwDQJN5YHk85AyHUM8bdj+9ZS2zMthCFJqciW8i4NcugjQ+QYYmWGqBeFYA==} + /@effect/docgen@0.3.4(tsx@4.1.2)(typescript@5.2.2): + resolution: {integrity: sha512-NO0CkND5oH9/pvqpViXARGjRxGD70iBpouimaPJ3nMpRi91gBLLwyWSxYeXoYN1Sva9WPq93O4DRSOmtnFQWNA==} engines: {node: '>=16.17.1'} hasBin: true peerDependencies: tsx: ^4.1.0 typescript: ^5.2.2 dependencies: - '@effect/platform-node': 0.29.4(@effect/schema@0.47.7)(effect@2.0.0-next.54) - '@effect/schema': 0.47.7(effect@2.0.0-next.54)(fast-check@3.13.2) - chalk: 5.3.0 doctrine: 3.0.0 - effect: 2.0.0-next.54 glob: 10.3.10 markdown-toc: github.com/effect-ts/markdown-toc/4bfeb0f140105440ea0d12df2fa23199cc3ec1d5 prettier: 3.1.0 - ts-morph: 20.0.0 - tsconfck: 3.0.0(typescript@5.2.2) tsx: 4.1.2 typescript: 5.2.2 - transitivePeerDependencies: - - fast-check dev: true /@effect/eslint-plugin@0.1.2: @@ -660,46 +697,21 @@ packages: resolution: {integrity: sha512-e8vfKbjnbYiyneBincEFS0tzXluopGK77OkVFbPRtUbNDS5tJfb+jiwOQEiqASDsadcZmd+9J9+Q6v/z7GuN2g==} dev: true - /@effect/platform-node@0.29.4(@effect/schema@0.47.7)(effect@2.0.0-next.54): - resolution: {integrity: sha512-a/YTVKssgL1d1g9MAApA2R2g5X9XuY55LK2nLVm7o8HRipWY6yb0+ieq3ODAUVB6uVrcC2js7P6eBAIma9/scA==} - peerDependencies: - effect: 2.0.0-next.54 - dependencies: - '@effect/platform': 0.28.4(@effect/schema@0.47.7)(effect@2.0.0-next.54) - busboy: 1.6.0 - effect: 2.0.0-next.54 - mime: 3.0.0 - transitivePeerDependencies: - - '@effect/schema' - dev: true - - /@effect/platform-node@0.31.0(@effect/schema@0.48.0)(effect@2.0.0-next.55): - resolution: {integrity: sha512-N+oUeIXtI/DCuhaXd9/UVJrvQbUtkc2g+YoeGmEIZovr4EgJX+6NFPV8auALqtNtXoucPjBZqALns8H7DMmnbw==} + /@effect/platform-node@0.31.2(@effect/schema@0.48.0)(effect@2.0.0-next.55): + resolution: {integrity: sha512-pNJfcgM8aYAxGt7ir2/qbN+LIAnN3O2TXK0akPEowptFRRUpvjEwDhOGcLUje/1BHk4b1bikmU1tLdPOFxivOg==} peerDependencies: effect: 2.0.0-next.55 dependencies: - '@effect/platform': 0.30.0(@effect/schema@0.48.0)(effect@2.0.0-next.55) + '@effect/platform': 0.30.2(@effect/schema@0.48.0)(effect@2.0.0-next.55) effect: 2.0.0-next.55 mime: 3.0.0 - multipasta: 0.1.15 + multipasta: 0.1.18 transitivePeerDependencies: - '@effect/schema' dev: true - /@effect/platform@0.28.4(@effect/schema@0.47.7)(effect@2.0.0-next.54): - resolution: {integrity: sha512-bVHY+LxttUttMNrYwRXTQ1sOsODQECawht6Af4dOq0M6tpZEX3dGL1hTzScNwSfzSwM4D84extfPipQTGjMJsg==} - peerDependencies: - '@effect/schema': ^0.47.3 - effect: 2.0.0-next.54 - dependencies: - '@effect/schema': 0.47.7(effect@2.0.0-next.54)(fast-check@3.13.2) - effect: 2.0.0-next.54 - find-my-way: 7.7.0 - path-browserify: 1.0.1 - dev: true - - /@effect/platform@0.30.0(@effect/schema@0.48.0)(effect@2.0.0-next.55): - resolution: {integrity: sha512-/MKZk7eI9guL6QD3XbcXG+EKWe2lPKJVoalVgt9wE3aznfWG+F5Zx0UBuw8gFhSx/IZKE4Mo7I2BLFt842m8dA==} + /@effect/platform@0.30.2(@effect/schema@0.48.0)(effect@2.0.0-next.55): + resolution: {integrity: sha512-NdvaINbYwmOpvRdfqlmEwdeD1fH8u1XkgUGoIjatWMn1GQr46xLcZAVZAakRjGLmlI0cPx9BWZfRjRaFgUcN2w==} peerDependencies: '@effect/schema': ^0.48.0 effect: 2.0.0-next.55 @@ -707,7 +719,7 @@ packages: '@effect/schema': 0.48.0(effect@2.0.0-next.55)(fast-check@3.13.2) effect: 2.0.0-next.55 find-my-way: 7.7.0 - multipasta: 0.1.15 + multipasta: 0.1.18 path-browserify: 1.0.1 dev: true @@ -732,16 +744,6 @@ packages: effect: 2.0.0-next.55 dev: true - /@effect/schema@0.47.7(effect@2.0.0-next.54)(fast-check@3.13.2): - resolution: {integrity: sha512-rQpzxfGsv2C3HSpwBRYPt7p5eibekWbn+RrYCd6SWtNSF0YPClhZwzBLygQvqDMXHkk3rHuuSOnJvIdqu7o4Hw==} - peerDependencies: - effect: 2.0.0-next.54 - fast-check: ^3.13.2 - dependencies: - effect: 2.0.0-next.54 - fast-check: 3.13.2 - dev: true - /@effect/schema@0.48.0(effect@2.0.0-next.55)(fast-check@3.13.2): resolution: {integrity: sha512-IpAik8hX4kAuOdfNBfxfE8mYVfiikheSxFyWRkVQUutD1E86hDXG90EUYu2/zL2pHkgoS6c2cnY7uoKYwKSP2w==} peerDependencies: @@ -769,6 +771,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.19.5: + resolution: {integrity: sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.18.20: resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -778,6 +789,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.19.5: + resolution: {integrity: sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.18.20: resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -787,6 +807,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.19.5: + resolution: {integrity: sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.18.20: resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -796,6 +825,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.19.5: + resolution: {integrity: sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.18.20: resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -805,6 +843,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.19.5: + resolution: {integrity: sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.18.20: resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -814,6 +861,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.19.5: + resolution: {integrity: sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.18.20: resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -823,6 +879,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.19.5: + resolution: {integrity: sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.18.20: resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -832,6 +897,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.19.5: + resolution: {integrity: sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.18.20: resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -841,6 +915,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.19.5: + resolution: {integrity: sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.18.20: resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -850,6 +933,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.19.5: + resolution: {integrity: sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.18.20: resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -859,6 +951,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.19.5: + resolution: {integrity: sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.18.20: resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -868,6 +969,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.19.5: + resolution: {integrity: sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.18.20: resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -877,6 +987,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.19.5: + resolution: {integrity: sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.18.20: resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -886,6 +1005,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.19.5: + resolution: {integrity: sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.18.20: resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -895,6 +1023,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.19.5: + resolution: {integrity: sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.18.20: resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -904,6 +1041,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.19.5: + resolution: {integrity: sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.18.20: resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -913,6 +1059,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.19.5: + resolution: {integrity: sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.18.20: resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -922,6 +1077,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.19.5: + resolution: {integrity: sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.18.20: resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -931,6 +1095,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.19.5: + resolution: {integrity: sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.18.20: resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -940,6 +1113,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.19.5: + resolution: {integrity: sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.18.20: resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -949,6 +1131,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.19.5: + resolution: {integrity: sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.18.20: resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -958,6 +1149,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.19.5: + resolution: {integrity: sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1046,7 +1246,7 @@ packages: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 '@types/node': 20.9.0 - '@types/yargs': 15.0.18 + '@types/yargs': 15.0.17 chalk: 4.1.2 dev: true @@ -1073,6 +1273,13 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jridgewell/trace-mapping@0.3.20: resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} dependencies: @@ -1083,7 +1290,7 @@ packages: /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 @@ -1092,7 +1299,7 @@ packages: /@manypkg/get-packages@1.1.3: resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -1127,9 +1334,105 @@ packages: fastq: 1.15.0 dev: true - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm-eabi@4.4.1: + resolution: {integrity: sha512-Ss4suS/sd+6xLRu+MLCkED2mUrAyqHmmvZB+zpzZ9Znn9S8wCkTQCJaQ8P8aHofnvG5L16u9MVnJjCqioPErwQ==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.4.1: + resolution: {integrity: sha512-sRSkGTvGsARwWd7TzC8LKRf8FiPn7257vd/edzmvG4RIr9x68KBN0/Ek48CkuUJ5Pj/Dp9vKWv6PEupjKWjTYA==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.4.1: + resolution: {integrity: sha512-nz0AiGrrXyaWpsmBXUGOBiRDU0wyfSXbFuF98pPvIO8O6auQsPG6riWsfQqmCCC5FNd8zKQ4JhgugRNAkBJ8mQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.4.1: + resolution: {integrity: sha512-Ogqvf4/Ve/faMaiPRvzsJEqajbqs00LO+8vtrPBVvLgdw4wBg6ZDXdkDAZO+4MLnrc8mhGV6VJAzYScZdPLtJg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.4.1: + resolution: {integrity: sha512-9zc2tqlr6HfO+hx9+wktUlWTRdje7Ub15iJqKcqg5uJZ+iKqmd2CMxlgPpXi7+bU7bjfDIuvCvnGk7wewFEhCg==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.4.1: + resolution: {integrity: sha512-phLb1fN3rq2o1j1v+nKxXUTSJnAhzhU0hLrl7Qzb0fLpwkGMHDem+o6d+ZI8+/BlTXfMU4kVWGvy6g9k/B8L6Q==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.4.1: + resolution: {integrity: sha512-M2sDtw4tf57VPSjbTAN/lz1doWUqO2CbQuX3L9K6GWIR5uw9j+ROKCvvUNBY8WUbMxwaoc8mH9HmmBKsLht7+w==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.4.1: + resolution: {integrity: sha512-mHIlRLX+hx+30cD6c4BaBOsSqdnCE4ok7/KDvjHYAHoSuveoMMxIisZFvcLhUnyZcPBXDGZTuBoalcuh43UfQQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.4.1: + resolution: {integrity: sha512-tB+RZuDi3zxFx7vDrjTNGVLu2KNyzYv+UY8jz7e4TMEoAj7iEt8Qk6xVu6mo3pgjnsHj6jnq3uuRsHp97DLwOA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.4.1: + resolution: {integrity: sha512-Hdn39PzOQowK/HZzYpCuZdJC91PE6EaGbTe2VCA9oq2u18evkisQfws0Smh9QQGNNRa/T7MOuGNQoLeXhhE3PQ==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.4.1: + resolution: {integrity: sha512-tLpKb1Elm9fM8c5w3nl4N1eLTP4bCqTYw9tqUBxX8/hsxqHO3dxc2qPbZ9PNkdK4tg4iLEYn0pOUnVByRd2CbA==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.4.1: + resolution: {integrity: sha512-eAhItDX9yQtZVM3yvXS/VR3qPqcnXvnLyx1pLXl4JzyNMBNO3KC986t/iAg2zcMzpAp9JSvxB5VZGnBiNoA98w==} + cpu: [x64] + os: [win32] requiresBuild: true dev: true optional: true @@ -1138,17 +1441,8 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@ts-morph/common@0.21.0: - resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} - dependencies: - fast-glob: 3.3.2 - minimatch: 7.4.6 - mkdirp: 2.1.6 - path-browserify: 1.0.1 - dev: true - - /@types/chai-subset@1.3.5: - resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==} + /@types/chai-subset@1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: '@types/chai': 4.3.10 dev: true @@ -1157,12 +1451,16 @@ packages: resolution: {integrity: sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==} dev: true - /@types/is-ci@3.0.4: - resolution: {integrity: sha512-AkCYCmwlXeuH89DagDCzvCAyltI2v9lh3U3DqSg/GrBYoReAaWwxfXCqMx9UV5MajLZ4ZFwZzV4cABGIxk2XRw==} + /@types/is-ci@3.0.2: + resolution: {integrity: sha512-9PyP1rgCro6xO3R7zOEoMgx5U9HpLhIg1FFb9p2mWX/x5QI8KMuCWWYtCT1dUQpicp84OsxEAw3iqwIKQY5Pog==} dependencies: ci-info: 3.9.0 dev: true + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: true + /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: true @@ -1179,6 +1477,10 @@ packages: '@types/istanbul-lib-report': 3.0.3 dev: true + /@types/json-schema@7.0.13: + resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -1187,8 +1489,8 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/minimist@1.2.5: - resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + /@types/minimist@1.2.3: + resolution: {integrity: sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==} dev: true /@types/node@12.20.55: @@ -1201,8 +1503,12 @@ packages: undici-types: 5.26.5 dev: true - /@types/normalize-package-data@2.4.4: - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + /@types/normalize-package-data@2.4.2: + resolution: {integrity: sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==} + dev: true + + /@types/semver@7.5.3: + resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} dev: true /@types/semver@7.5.5: @@ -1213,14 +1519,14 @@ packages: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true - /@types/yargs-parser@21.0.3: - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + /@types/yargs-parser@21.0.2: + resolution: {integrity: sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==} dev: true - /@types/yargs@15.0.18: - resolution: {integrity: sha512-DDi2KmvAnNsT/EvU8jp1UR7pOJojBtJ3GLZ/uw1MUq4VbbESppPWoHUY4h0OB4BbEbGJiyEsmUcuZDZtoR+ZwQ==} + /@types/yargs@15.0.17: + resolution: {integrity: sha512-cj53I8GUcWJIgWVTSVe2L7NJAB5XWGdsoMosVvUgv1jEnMbAcsbaCzt1coUcyi8Sda5PgTWAooG8jNyDTD+CWA==} dependencies: - '@types/yargs-parser': 21.0.3 + '@types/yargs-parser': 21.0.2 dev: true /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2): @@ -1281,6 +1587,14 @@ packages: '@typescript-eslint/visitor-keys': 6.11.0 dev: true + /@typescript-eslint/scope-manager@6.7.5: + resolution: {integrity: sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 + dev: true + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1316,6 +1630,11 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@6.7.5: + resolution: {integrity: sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree@4.33.0(typescript@3.9.10): resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1379,6 +1698,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.7.5(typescript@5.2.2): + resolution: {integrity: sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1398,6 +1738,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.7.5(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.13 + '@types/semver': 7.5.3 + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + eslint: 8.53.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@4.33.0: resolution: {integrity: sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} @@ -1422,6 +1781,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@6.7.5: + resolution: {integrity: sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.7.5 + eslint-visitor-keys: 3.4.3 + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -1433,13 +1800,13 @@ packages: dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 - istanbul-lib-coverage: 3.2.2 + istanbul-lib-coverage: 3.2.0 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.6 - magic-string: 0.30.5 + magic-string: 0.30.4 picocolors: 1.0.0 - std-env: 3.5.0 + std-env: 3.4.3 test-exclude: 6.0.0 v8-to-istanbul: 9.1.3 vitest: 0.34.6 @@ -1481,7 +1848,7 @@ packages: resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: diff-sequences: 29.6.3 - loupe: 2.3.7 + loupe: 2.3.6 pretty-format: 29.7.0 dev: true @@ -1493,8 +1860,8 @@ packages: acorn: 8.11.2 dev: true - /acorn-walk@8.3.0: - resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} dev: true @@ -1583,7 +1950,7 @@ packages: /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 is-array-buffer: 3.0.2 dev: true @@ -1618,10 +1985,10 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 dev: true /array.prototype.flatmap@1.3.2: @@ -1639,10 +2006,10 @@ packages: engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.0 - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 dev: true @@ -1750,8 +2117,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001562 - electron-to-chromium: 1.4.583 + caniuse-lite: 1.0.30001561 + electron-to-chromium: 1.4.580 node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: true @@ -1767,18 +2134,18 @@ packages: ieee754: 1.2.1 dev: true - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: true - /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} dev: true + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: @@ -1806,8 +2173,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001562: - resolution: {integrity: sha512-kfte3Hym//51EdX4239i+Rmp20EsLIYGdPkERegTgU19hQWCRhsRFGKHTliUlsry53tv17K7n077Kqa0WJU4ng==} + /caniuse-lite@1.0.30001561: + resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} dev: true /chai@4.3.10: @@ -1818,7 +2185,7 @@ packages: check-error: 1.0.3 deep-eql: 4.1.3 get-func-name: 2.0.2 - loupe: 2.3.7 + loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 dev: true @@ -1840,11 +2207,6 @@ packages: supports-color: 7.2.0 dev: true - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true - /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -1911,10 +2273,6 @@ packages: engines: {node: '>=0.8'} dev: true - /code-block-writer@12.0.0: - resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} - dev: true - /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -2088,6 +2446,15 @@ packages: clone: 1.0.4 dev: true + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} @@ -2101,8 +2468,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 - has-property-descriptors: 1.0.1 + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 object-keys: 1.1.1 dev: true @@ -2329,16 +2696,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true - /effect@2.0.0-next.54: - resolution: {integrity: sha512-qROhKMxlm6fpa90YRfWSgKeelDfhaDq2igPK+pIKupGehiCnZH4vd2qrY71HVZ10qZgXxh0VXpGyDQxJC+EQqw==} - dev: true - /effect@2.0.0-next.55: resolution: {integrity: sha512-kNOB/HaDSdaFSDVXK2gyf4XcTKbKKlllyoS871uDyoJskcc+024sSOHnNioXZPgkqaz55a3mad4DFfLUIETwYQ==} dev: true - /electron-to-chromium@1.4.583: - resolution: {integrity: sha512-93y1gcONABZ7uqYe/JWDVQP/Pj/sQSunF0HVAPdlg/pfBnOyBMLlQUxWvkqcljJg1+W6cjvPuYD+r1Th9Tn8mA==} + /electron-to-chromium@1.4.580: + resolution: {integrity: sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==} dev: true /emoji-regex@8.0.0: @@ -2377,6 +2740,51 @@ packages: stackframe: 1.3.4 dev: true + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.4 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + /es-abstract@1.22.3: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} @@ -2422,6 +2830,15 @@ packages: which-typed-array: 1.1.13 dev: true + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + has-tostringtag: 1.0.0 + dev: true + /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} @@ -2431,6 +2848,12 @@ packages: hasown: 2.0.0 dev: true + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.4 + dev: true + /es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: @@ -2476,6 +2899,36 @@ packages: '@esbuild/win32-x64': 0.18.20 dev: true + /esbuild@0.19.5: + resolution: {integrity: sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.5 + '@esbuild/android-arm64': 0.19.5 + '@esbuild/android-x64': 0.19.5 + '@esbuild/darwin-arm64': 0.19.5 + '@esbuild/darwin-x64': 0.19.5 + '@esbuild/freebsd-arm64': 0.19.5 + '@esbuild/freebsd-x64': 0.19.5 + '@esbuild/linux-arm': 0.19.5 + '@esbuild/linux-arm64': 0.19.5 + '@esbuild/linux-ia32': 0.19.5 + '@esbuild/linux-loong64': 0.19.5 + '@esbuild/linux-mips64el': 0.19.5 + '@esbuild/linux-ppc64': 0.19.5 + '@esbuild/linux-riscv64': 0.19.5 + '@esbuild/linux-s390x': 0.19.5 + '@esbuild/linux-x64': 0.19.5 + '@esbuild/netbsd-x64': 0.19.5 + '@esbuild/openbsd-x64': 0.19.5 + '@esbuild/sunos-x64': 0.19.5 + '@esbuild/win32-arm64': 0.19.5 + '@esbuild/win32-ia32': 0.19.5 + '@esbuild/win32-x64': 0.19.5 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -2530,9 +2983,9 @@ packages: eslint: 8.53.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0) eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0) - fast-glob: 3.3.2 + fast-glob: 3.3.1 get-tsconfig: 4.7.2 - is-core-module: 2.13.1 + is-core-module: 2.13.0 is-glob: 4.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -2577,8 +3030,8 @@ packages: dependencies: '@babel/core': 7.23.3 '@babel/generator': 7.12.17 - '@babel/parser': 7.23.3 - '@babel/traverse': 7.23.3 + '@babel/parser': 7.23.0 + '@babel/traverse': 7.23.2 expect: 26.6.2 fp-ts: 2.16.1 glob: 10.3.10 @@ -2598,7 +3051,7 @@ packages: eslint: ^7.0.0 || ^8.0.0 typescript: ^4.2.4 || ^5.0.0 dependencies: - '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.5(eslint@8.53.0)(typescript@5.2.2) eslint: 8.53.0 tslib: 2.6.2 tsutils: 3.21.0(typescript@5.2.2) @@ -2818,8 +3271,8 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2853,7 +3306,7 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flat-cache: 3.2.0 + flat-cache: 3.1.1 dev: true /filing-cabinet@3.3.1: @@ -2868,7 +3321,7 @@ packages: is-relative-path: 1.0.2 module-definition: 3.4.0 module-lookup-amd: 7.0.1 - resolve: 1.22.8 + resolve: 1.22.6 resolve-dependency-path: 2.0.0 sass-lookup: 3.0.0 stylus-lookup: 3.0.2 @@ -2929,9 +3382,9 @@ packages: pkg-dir: 4.2.0 dev: true - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + /flat-cache@3.1.1: + resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} + engines: {node: '>=12.0.0'} dependencies: flatted: 3.2.9 keyv: 4.5.4 @@ -3004,6 +3457,10 @@ packages: dev: true optional: true + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true @@ -3012,9 +3469,9 @@ packages: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 functions-have-names: 1.2.3 dev: true @@ -3052,6 +3509,15 @@ packages: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.4 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} dependencies: @@ -3069,8 +3535,8 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 dev: true /get-tsconfig@4.7.2: @@ -3141,7 +3607,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.2 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -3158,7 +3624,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.1 dev: true /graceful-fs@4.2.11: @@ -3211,6 +3677,12 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + /has-property-descriptors@1.0.1: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: @@ -3234,6 +3706,11 @@ packages: has-symbols: 1.0.3 dev: true + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + dev: true + /hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} @@ -3306,6 +3783,15 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: true + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + side-channel: 1.0.4 + dev: true + /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -3333,8 +3819,8 @@ packages: /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 is-typed-array: 1.1.12 dev: true @@ -3361,7 +3847,7 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 has-tostringtag: 1.0.0 dev: true @@ -3381,6 +3867,12 @@ packages: ci-info: 3.9.0 dev: true + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.4 + dev: true + /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: @@ -3484,7 +3976,7 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 has-tostringtag: 1.0.0 dev: true @@ -3500,7 +3992,7 @@ packages: /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 dev: true /is-string@1.0.7: @@ -3528,7 +4020,7 @@ packages: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: - which-typed-array: 1.1.13 + which-typed-array: 1.1.11 dev: true /is-unicode-supported@0.1.0: @@ -3548,7 +4040,7 @@ packages: /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 dev: true /is-windows@1.0.2: @@ -3580,8 +4072,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} dev: true @@ -3589,7 +4081,7 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} dependencies: - istanbul-lib-coverage: 3.2.2 + istanbul-lib-coverage: 3.2.0 make-dir: 4.0.0 supports-color: 7.2.0 dev: true @@ -3599,7 +4091,7 @@ packages: engines: {node: '>=10'} dependencies: debug: 4.3.4 - istanbul-lib-coverage: 3.2.2 + istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: - supports-color @@ -3854,8 +4346,8 @@ packages: is-unicode-supported: 0.1.0 dev: true - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + /loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: get-func-name: 2.0.2 dev: true @@ -3924,6 +4416,13 @@ packages: - supports-color dev: true + /magic-string@0.30.4: + resolution: {integrity: sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} @@ -3969,7 +4468,7 @@ packages: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} dependencies: - '@types/minimist': 1.2.5 + '@types/minimist': 1.2.3 camelcase-keys: 6.2.2 decamelize-keys: 1.1.1 hard-rejection: 2.1.0 @@ -4017,13 +4516,6 @@ packages: brace-expansion: 1.1.11 dev: true - /minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} - engines: {node: '>=10'} - dependencies: - brace-expansion: 2.0.1 - dev: true - /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -4062,12 +4554,6 @@ packages: engines: {node: '>= 8.0.0'} dev: true - /mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - dev: true - /mlly@1.4.2: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: @@ -4117,8 +4603,8 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true - /multipasta@0.1.15: - resolution: {integrity: sha512-mxwpIFmpBRFJfoOU2DJdBd/rL3rf5RWsd7OamXqaukWudWkIYcsOfVABLuVG1Tf9qhsVWMIPwSa6Mgj5j1Hgwg==} + /multipasta@0.1.18: + resolution: {integrity: sha512-lY1GMsEcZEcdv2dSQ0R9JVRDYyvx7aWZ1RDbUc3K42H6CGbrkGuODFq9jLAx7rcDIbMn/lT+kgVA9HAGxIXiCw==} dev: true /nanoid@3.3.7: @@ -4155,21 +4641,21 @@ packages: resolution: {integrity: sha512-8Q1hXew6ETzqKRAs3jjLioSxNfT1cx74ooiF8RlAONwVMcfq+UdzLC2eB5qcPldUxaE5w3ytLkrmV1TGddhZTA==} engines: {node: '>=6.0'} dependencies: - '@babel/parser': 7.23.3 + '@babel/parser': 7.23.0 dev: true /node-source-walk@5.0.2: resolution: {integrity: sha512-Y4jr/8SRS5hzEdZ7SGuvZGwfORvNsSsNRwDXx5WisiqzsVfeftDvRgfeqWNgZvWSJbgubTRVRYBzK6UO+ErqjA==} engines: {node: '>=12'} dependencies: - '@babel/parser': 7.23.3 + '@babel/parser': 7.23.0 dev: true /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.8 + resolve: 1.22.6 semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -4181,6 +4667,10 @@ packages: dev: true optional: true + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true @@ -4194,7 +4684,7 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -4631,7 +5121,7 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} dependencies: - '@types/normalize-package-data': 2.4.4 + '@types/normalize-package-data': 2.4.2 normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 @@ -4693,7 +5183,7 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 set-function-name: 2.0.1 dev: true @@ -4759,6 +5249,15 @@ packages: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -4793,11 +5292,31 @@ packages: glob: 7.2.3 dev: true - /rollup@3.29.4: - resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: true + + /rollup@4.4.1: + resolution: {integrity: sha512-idZzrUpWSblPJX66i+GzrpjKE3vbYrlWirUHteoAbjKReZwa0cohAErOYA5efoMmNCdvG9yrJS+w9Kl6csaH4w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.4.1 + '@rollup/rollup-android-arm64': 4.4.1 + '@rollup/rollup-darwin-arm64': 4.4.1 + '@rollup/rollup-darwin-x64': 4.4.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.4.1 + '@rollup/rollup-linux-arm64-gnu': 4.4.1 + '@rollup/rollup-linux-arm64-musl': 4.4.1 + '@rollup/rollup-linux-x64-gnu': 4.4.1 + '@rollup/rollup-linux-x64-musl': 4.4.1 + '@rollup/rollup-win32-arm64-msvc': 4.4.1 + '@rollup/rollup-win32-ia32-msvc': 4.4.1 + '@rollup/rollup-win32-x64-msvc': 4.4.1 fsevents: 2.3.3 dev: true @@ -4811,8 +5330,8 @@ packages: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 has-symbols: 1.0.3 isarray: 2.0.5 dev: true @@ -4828,8 +5347,8 @@ packages: /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 is-regex: 1.1.4 dev: true @@ -4887,9 +5406,9 @@ packages: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 + define-data-property: 1.1.0 functions-have-names: 1.2.3 - has-property-descriptors: 1.0.1 + has-property-descriptors: 1.0.0 dev: true /set-getter@0.1.1: @@ -4926,9 +5445,9 @@ packages: /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - object-inspect: 1.13.1 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 dev: true /siginfo@2.0.0: @@ -5037,8 +5556,8 @@ packages: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} dev: true - /std-env@3.5.0: - resolution: {integrity: sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==} + /std-env@3.4.3: + resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} dev: true /stream-to-array@2.3.0: @@ -5053,11 +5572,6 @@ packages: mixme: 0.5.9 dev: true - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: true - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5094,25 +5608,25 @@ packages: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 dev: true /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 dev: true /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 dev: true /string_decoder@1.1.1: @@ -5312,26 +5826,6 @@ packages: engines: {node: '>=14.16'} dev: true - /ts-morph@20.0.0: - resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==} - dependencies: - '@ts-morph/common': 0.21.0 - code-block-writer: 12.0.0 - dev: true - - /tsconfck@3.0.0(typescript@5.2.2): - resolution: {integrity: sha512-w3wnsIrJNi7avf4Zb0VjOoodoO0woEqGgZGQm+LHH9przdUI+XDKsWAXwxHA1DaRTjeuZNcregSzr7RaA8zG9A==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - dependencies: - typescript: 5.2.2 - dev: true - /tsconfig-paths@3.14.2: resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} dependencies: @@ -5391,8 +5885,8 @@ packages: fsevents: 2.3.3 dev: true - /tty-table@4.2.3: - resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==} + /tty-table@4.2.2: + resolution: {integrity: sha512-2gvCArMZLxgvpZ2NvQKdnYWIFLe7I/z5JClMuhrDXunmKgSZcQKcZRjN9XjAFiToMz2pUo1dEIXyrm0AwgV5Tw==} engines: {node: '>=8.0.0'} hasBin: true dependencies: @@ -5441,8 +5935,8 @@ packages: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 is-typed-array: 1.1.12 dev: true @@ -5450,7 +5944,7 @@ packages: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -5461,7 +5955,7 @@ packages: engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.5 + call-bind: 1.0.2 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -5470,7 +5964,7 @@ packages: /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 for-each: 0.3.3 is-typed-array: 1.1.12 dev: true @@ -5504,7 +5998,7 @@ packages: /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 @@ -5548,8 +6042,8 @@ packages: resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.20 - '@types/istanbul-lib-coverage': 2.0.6 + '@jridgewell/trace-mapping': 0.3.19 + '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 2.0.0 dev: true @@ -5570,7 +6064,7 @@ packages: mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.5.0(@types/node@20.9.0) + vite: 5.0.0(@types/node@20.9.0) transitivePeerDependencies: - '@types/node' - less @@ -5582,12 +6076,12 @@ packages: - terser dev: true - /vite@4.5.0(@types/node@20.9.0): - resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} - engines: {node: ^14.18.0 || >=16.0.0} + /vite@5.0.0(@types/node@20.9.0): + resolution: {integrity: sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: - '@types/node': '>= 14' + '@types/node': ^18.0.0 || >=20.0.0 less: '*' lightningcss: ^1.21.0 sass: '*' @@ -5611,9 +6105,9 @@ packages: optional: true dependencies: '@types/node': 20.9.0 - esbuild: 0.18.20 + esbuild: 0.19.5 postcss: 8.4.31 - rollup: 3.29.4 + rollup: 4.4.1 optionalDependencies: fsevents: 2.3.3 dev: true @@ -5650,7 +6144,7 @@ packages: optional: true dependencies: '@types/chai': 4.3.10 - '@types/chai-subset': 1.3.5 + '@types/chai-subset': 1.3.3 '@types/node': 20.9.0 '@vitest/expect': 0.34.6 '@vitest/runner': 0.34.6 @@ -5658,7 +6152,7 @@ packages: '@vitest/spy': 0.34.6 '@vitest/utils': 0.34.6 acorn: 8.11.2 - acorn-walk: 8.3.0 + acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.10 debug: 4.3.4 @@ -5666,11 +6160,11 @@ packages: magic-string: 0.30.5 pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.5.0 + std-env: 3.4.3 strip-literal: 1.3.0 tinybench: 2.5.1 tinypool: 0.7.0 - vite: 4.5.0(@types/node@20.9.0) + vite: 5.0.0(@types/node@20.9.0) vite-node: 0.34.6(@types/node@20.9.0) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -5727,6 +6221,17 @@ packages: path-exists: 4.0.0 dev: true + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + /which-typed-array@1.1.13: resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} engines: {node: '>= 0.4'} diff --git a/src/Args.ts b/src/Args.ts index 50617bb..c4a83ca 100644 --- a/src/Args.ts +++ b/src/Args.ts @@ -2,6 +2,7 @@ * @since 1.0.0 */ import type { FileSystem } from "@effect/platform/FileSystem" +import type { Terminal } from "@effect/platform/Terminal" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { Option } from "effect/Option" @@ -10,7 +11,6 @@ import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type { CliConfig } from "./CliConfig.js" import type { HelpDoc } from "./HelpDoc.js" import * as InternalArgs from "./internal/args.js" -import type { Parameter } from "./Parameter.js" import type { Primitive } from "./Primitive.js" import type { RegularLanguage } from "./RegularLanguage.js" import type { Usage } from "./Usage.js" @@ -34,11 +34,13 @@ export type ArgsTypeId = typeof ArgsTypeId * @since 1.0.0 * @category models */ -export interface Args extends Args.Variance, Parameter, Pipeable { - get maxSize(): number - get minSize(): number - get identifier(): Option - get usage(): Usage +export interface Args extends Args.Variance, Pipeable { + help(): HelpDoc + usage(): Usage + maxSize(): number + minSize(): number + identifier(): Option + wizard(config: CliConfig): Effect> validate( args: ReadonlyArray, config: CliConfig diff --git a/src/BuiltInOptions.ts b/src/BuiltInOptions.ts index 1cfb4e8..75236a7 100644 --- a/src/BuiltInOptions.ts +++ b/src/BuiltInOptions.ts @@ -52,7 +52,7 @@ export interface ShowCompletions { */ export interface ShowWizard { readonly _tag: "ShowWizard" - readonly commmand: Command + readonly command: Command } /** @@ -120,5 +120,5 @@ export const showHelp: (usage: Usage, helpDoc: HelpDoc) => BuiltInOptions = * @since 1.0.0 * @category constructors */ -export const showWizard: (commmand: Command) => BuiltInOptions = +export const showWizard: (command: Command) => BuiltInOptions = InternalBuiltInOptions.showWizard diff --git a/src/CliApp.ts b/src/CliApp.ts index 7d9ad16..dff4930 100644 --- a/src/CliApp.ts +++ b/src/CliApp.ts @@ -4,6 +4,7 @@ import type { CommandExecutor } from "@effect/platform/CommandExecutor" import type { FileSystem } from "@effect/platform/FileSystem" import type { Path } from "@effect/platform/Path" +import type { Terminal } from "@effect/platform/Terminal" import type { Effect } from "effect/Effect" import type { Command } from "./Command.js" import type { HelpDoc } from "./HelpDoc.js" @@ -33,7 +34,7 @@ export declare namespace CliApp { * @since 1.0.0 * @category models */ - export type Environment = CommandExecutor | FileSystem | Path + export type Environment = CommandExecutor | FileSystem | Path | Terminal } /** diff --git a/src/Command.ts b/src/Command.ts index 9600333..5c5e168 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -2,9 +2,11 @@ * @since 1.0.0 */ import type { FileSystem } from "@effect/platform/FileSystem" +import type { Terminal } from "@effect/platform/Terminal" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { HashMap } from "effect/HashMap" +import type { HashSet } from "effect/HashSet" import type { Option } from "effect/Option" import type { Pipeable } from "effect/Pipeable" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" @@ -14,10 +16,8 @@ import type { CommandDirective } from "./CommandDirective.js" import type { HelpDoc } from "./HelpDoc.js" import * as InternalCommand from "./internal/command.js" import type { Options } from "./Options.js" -import type { Named } from "./Parameter.js" import type { Prompt } from "./Prompt.js" import type { RegularLanguage } from "./RegularLanguage.js" -import type { Terminal } from "./Terminal.js" import type { Usage } from "./Usage.js" import type { ValidationError } from "./ValidationError.js" @@ -43,9 +43,12 @@ export type CommandTypeId = typeof CommandTypeId * @since 1.0.0 * @category models */ -export interface Command extends Command.Variance, Named, Pipeable { - get usage(): Usage - get subcommands(): HashMap> +export interface Command extends Command.Variance, Pipeable { + help(): HelpDoc + usage(): Usage + names(): HashSet + subcommands(): HashMap> + wizard(config: CliConfig): Effect> parse( args: ReadonlyArray, config: CliConfig diff --git a/src/Compgen.ts b/src/Compgen.ts index b1fca8c..1c9d337 100644 --- a/src/Compgen.ts +++ b/src/Compgen.ts @@ -29,11 +29,11 @@ export const Compgen: Tag = InternalCompgen.Tag * @since 1.0.0 * @category context */ -export const LiveCompgen: Layer = InternalCompgen.LiveCompgen +export const layer: Layer = InternalCompgen.layer /** * @since 1.0.0 * @category context */ -export const TestCompgen: (workingDirectory: string) => Layer = - InternalCompgen.TestCompgen +export const testLayer: (workingDirectory: string) => Layer = + InternalCompgen.testLayer diff --git a/src/Options.ts b/src/Options.ts index 649e3eb..be532e3 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -2,6 +2,7 @@ * @since 1.0.0 */ import type { FileSystem } from "@effect/platform/FileSystem" +import type { Terminal } from "@effect/platform/Terminal" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { HashMap } from "effect/HashMap" @@ -11,7 +12,6 @@ import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type { CliConfig } from "./CliConfig.js" import type { HelpDoc } from "./HelpDoc.js" import * as InternalOptions from "./internal/options.js" -import type { Input, Parameter } from "./Parameter.js" import type { Primitive } from "./Primitive.js" import type { RegularLanguage } from "./RegularLanguage.js" import type { Usage } from "./Usage.js" @@ -33,16 +33,17 @@ export type OptionsTypeId = typeof OptionsTypeId * @since 1.0.0 * @category models */ -export interface Options extends Options.Variance, Parameter, Pipeable { - get identifier(): Option - get usage(): Usage - get flattened(): ReadonlyArray +export interface Options extends Options.Variance, Pipeable { + help(): HelpDoc + usage(): Usage + identifier(): Option + flattened(): ReadonlyArray> + wizard(config: CliConfig): Effect> + modifySingle(f: <_>(single: InternalOptions.Single<_>) => InternalOptions.Single<_>): Options validate( args: HashMap>, config: CliConfig ): Effect - /** @internal */ - modifySingle(f: <_>(single: InternalOptions.Single<_>) => InternalOptions.Single<_>): Options } /** @@ -59,6 +60,17 @@ export declare namespace Options { } } + /** + * @since 1.0.0 + * @category models + */ + export interface ParseableOptions extends Options { + parse( + args: ReadonlyArray, + config: CliConfig + ): Effect, ReadonlyArray]> + } + /** * @since 1.0.0 * @category models diff --git a/src/Parameter.ts b/src/Parameter.ts deleted file mode 100644 index 14d33fa..0000000 --- a/src/Parameter.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @since 1.0.0 - */ -import type { FileSystem } from "@effect/platform/FileSystem" -import type { Effect } from "effect/Effect" -import type { HashSet } from "effect/HashSet" -import type { CliConfig } from "./CliConfig.js" -import type { HelpDoc } from "./HelpDoc.js" -import type { ValidationError } from "./ValidationError.js" - -/** - * Abstraction employed by Wizard class. Parameter trait encompass `Command`, - * `Options` and `Args` interfaces. - * - * The `Wizard` processes subtypes of `Parameter` in different manners. - * - * @since 1.0.0 - */ -export interface Parameter { - get help(): HelpDoc - get shortDescription(): string -} - -/** - * Input is used to obtain a parameter from user. - * - * @since 1.0.0 - */ -export interface Input extends Parameter { - isValid( - input: string, - config: CliConfig - ): Effect> - parse( - args: ReadonlyArray, - config: CliConfig - ): Effect, ReadonlyArray]> -} - -/** - * Represent a parameter with name to be used as the options in Alternatives. - * - * @since 1.0.0 - */ -export interface Named extends Parameter { - get names(): HashSet -} diff --git a/src/Primitive.ts b/src/Primitive.ts index 2474440..849d57f 100644 --- a/src/Primitive.ts +++ b/src/Primitive.ts @@ -7,8 +7,10 @@ import type { Option } from "effect/Option" import type { Pipeable } from "effect/Pipeable" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type { CliConfig } from "./CliConfig.js" +import type { HelpDoc } from "./HelpDoc.js" import type { Span } from "./HelpDoc/Span.js" import * as InternalPrimitive from "./internal/primitive.js" +import type { Prompt } from "./Prompt.js" /** * @since 1.0.0 @@ -31,9 +33,10 @@ export type PrimitiveTypeId = typeof PrimitiveTypeId * @category models */ export interface Primitive extends Primitive.Variance { - get typeName(): string - get help(): Span - get choices(): Option + typeName(): string + help(): Span + choices(): Option + wizard(help: HelpDoc): Prompt validate(value: Option, config: CliConfig): Effect } diff --git a/src/Prompt.ts b/src/Prompt.ts index 7cab8fe..a3333ad 100644 --- a/src/Prompt.ts +++ b/src/Prompt.ts @@ -1,19 +1,25 @@ /** * @since 1.0.0 */ -import type { Effect, Pipeable } from "effect" -import * as internal from "./internal/prompt.js" -import * as numberPrompt from "./internal/prompt/number.js" -import * as selectPrompt from "./internal/prompt/select.js" -import * as textPrompt from "./internal/prompt/text.js" +import type { Terminal, UserInput } from "@effect/platform/Terminal" +import type { Effect } from "effect/Effect" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import * as InternalPrompt from "./internal/prompt.js" +import * as InternalConfirmPrompt from "./internal/prompt/confirm.js" +import * as InternalDatePrompt from "./internal/prompt/date.js" +import * as InternalListPrompt from "./internal/prompt/list.js" +import * as InternalNumberPrompt from "./internal/prompt/number.js" +import * as InternalSelectPrompt from "./internal/prompt/select.js" +import * as InternalTextPrompt from "./internal/prompt/text.js" +import * as InternalTogglePrompt from "./internal/prompt/toggle.js" import type { PromptAction } from "./Prompt/Action.js" -import type { Terminal } from "./Terminal.js" /** * @since 1.0.0 * @category symbols */ -export const PromptTypeId: unique symbol = internal.PromptTypeId +export const PromptTypeId: unique symbol = InternalPrompt.PromptTypeId /** * @since 1.0.0 @@ -26,7 +32,7 @@ export type PromptTypeId = typeof PromptTypeId * @category models */ export interface Prompt - extends Prompt.Variance, Pipeable.Pipeable, Effect.Effect + extends Prompt.Variance, Pipeable, Effect {} /** @@ -55,17 +61,153 @@ export declare namespace Prompt { */ export type Action = PromptAction + /** + * @since 1.0.0 + * @category models + */ + export interface ConfirmOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The intitial value of the confirm prompt (defaults to `false`). + */ + readonly initial?: boolean + /** + * The label to display after a user has responded to the prompt. + */ + readonly label?: { + /** + * The label used if the prompt is confirmed (defaults to `"yes"`). + */ + readonly confirm: string + /** + * The label used if the prompt is not confirmed (defaults to `"no"`). + */ + readonly deny: string + } + /** + * The placeholder to display when a user is responding to the prompt. + */ + readonly placeholder?: { + /** + * The placeholder to use if the `initial` value of the prompt is `true` + * (defaults to `"(Y/n)"`). + */ + readonly defaultConfirm?: string + /** + * The placeholder to use if the `initial` value of the prompt is `false` + * (defaults to `"(y/N)"`). + */ + readonly defaultDeny?: string + } + } + + /** + * @since 1.0.0 + * @category models + */ + export interface DateOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The initial date value to display in the prompt (defaults to the current + * date). + */ + readonly initial?: globalThis.Date + /** + * The format mask of the date (defaults to `YYYY-MM-DD HH:mm:ss`). + */ + readonly dateMask?: string + /** + * An effectful function that can be used to validate the value entered into + * the prompt before final submission. + */ + readonly validate?: (value: globalThis.Date) => Effect + /** + * Custom locales that can be used in place of the defaults. + */ + readonly locales?: { + /** + * The full names of each month of the year. + */ + readonly months: [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string + ] + /** + * The short names of each month of the year. + */ + readonly monthsShort: [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string + ] + /** + * The full names of each day of the week. + */ + readonly weekdays: [string, string, string, string, string, string, string] + /** + * The short names of each day of the week. + */ + readonly weekdaysShort: [string, string, string, string, string, string, string] + } + } + /** * @since 1.0.0 * @category models */ export interface IntegerOptions { + /** + * The message to display in the prompt. + */ readonly message: string + /** + * The minimum value that can be entered by the user (defaults to `-Infinity`). + */ readonly min?: number + /** + * The maximum value that can be entered by the user (defaults to `Infinity`). + */ readonly max?: number + /** + * The value that will be used to increment the prompt value when using the + * up arrow key (defaults to `1`). + */ readonly incrementBy?: number + /** + * The value that will be used to decrement the prompt value when using the + * down arrow key (defaults to `1`). + */ readonly decrementBy?: number - readonly validate?: (value: number) => Effect.Effect + /** + * An effectful function that can be used to validate the value entered into + * the prompt before final submission. + */ + readonly validate?: (value: number) => Effect } /** @@ -73,6 +215,9 @@ export declare namespace Prompt { * @category models */ export interface FloatOptions extends IntegerOptions { + /** + * The precision to use for the floating point value (defaults to `2`). + */ readonly precision?: number } @@ -80,13 +225,54 @@ export declare namespace Prompt { * @since 1.0.0 * @category models */ - export interface SelectOptions { + export interface ListOptions extends TextOptions { + /** + * The delimiter that separates list entries. + */ + readonly delimiter?: string + } + + /** + * @since 1.0.0 + * @category models + */ + export interface SelectOptions { + /** + * The message to display in the prompt. + */ readonly message: string - readonly choices: ReadonlyArray<{ - readonly title: string - readonly description?: string - readonly value: string - }> + /** + * The choices to display to the user. + */ + readonly choices: ReadonlyArray> + /** + * The number of choices to display at one time (defaults to `10`). + */ + readonly maxPerPage?: number + } + + /** + * @since 1.0.0 + * @category models + */ + export interface SelectChoice { + /** + * The name of the select option that is displayed to the user. + */ + readonly title: string + /** + * The underlying value of the select option. + */ + readonly value: A + /** + * An optional description for the select option which will be displayed + * to the user. + */ + readonly description?: string + /** + * Whether or not this select option is disabled. + */ + readonly disabled?: boolean } /** @@ -94,10 +280,48 @@ export declare namespace Prompt { * @category models */ export interface TextOptions { + /** + * The message to display in the prompt. + */ readonly message: string + /** + * The type of the text option. + */ readonly type?: "hidden" | "password" | "text" + /** + * The default value of the text option. + */ readonly default?: string - readonly validate?: (value: string) => Effect.Effect + /** + * An effectful function that can be used to validate the value entered into + * the prompt before final submission. + */ + readonly validate?: (value: string) => Effect + } + + /** + * @since 1.0.0 + * @category models + */ + export interface ToggleOptions { + /** + * The message to display in the prompt. + */ + readonly message: string + /** + * The intitial value of the toggle prompt (defaults to `false`). + */ + readonly initial?: boolean + /** + * The text to display when the toggle is in the active state (defaults to + * `on`). + */ + readonly active?: string + /** + * The text to display when the toggle is in the inactive state (defaults to + * `off`). + */ + readonly inactive?: string } } @@ -146,7 +370,14 @@ export declare namespace All { */ export const all: < const Arg extends Iterable> ->(arg: Arg) => All.Return = internal.all +>(arg: Arg) => All.Return = InternalPrompt.all + +/** + * @since 1.0.0 + * @category constructors + */ +export const confirm: (options: Prompt.ConfirmOptions) => Prompt = + InternalConfirmPrompt.confirm /** * Creates a custom `Prompt` from the provided `render` and `process` functions @@ -162,14 +393,21 @@ export const all: < export const custom: ( initialState: State, render: ( - state: State, + prevState: Option, + nextState: State, action: Prompt.Action - ) => Effect.Effect, + ) => Effect, process: ( - input: Terminal.UserInput, + input: UserInput, state: State - ) => Effect.Effect> -) => Prompt = internal.custom + ) => Effect> +) => Prompt = InternalPrompt.custom + +/** + * @since 1.0.0 + * @category constructors + */ +export const date: (options: Prompt.DateOptions) => Prompt = InternalDatePrompt.date /** * @since 1.0.0 @@ -180,19 +418,27 @@ export const flatMap: { f: (output: Output) => Prompt ): (self: Prompt) => Prompt (self: Prompt, f: (output: Output) => Prompt): Prompt -} = internal.flatMap +} = InternalPrompt.flatMap /** * @since 1.0.0 * @category constructors */ -export const float: (options: Prompt.FloatOptions) => Prompt = numberPrompt.float +export const float: (options: Prompt.FloatOptions) => Prompt = InternalNumberPrompt.float /** * @since 1.0.0 * @category constructors */ -export const integer: (options: Prompt.IntegerOptions) => Prompt = numberPrompt.integer +export const integer: (options: Prompt.IntegerOptions) => Prompt = + InternalNumberPrompt.integer + +/** + * @since 1.0.0 + * @category constructors + */ +export const list: (options: Prompt.ListOptions) => Prompt> = + InternalListPrompt.list /** * @since 1.0.0 @@ -201,7 +447,7 @@ export const integer: (options: Prompt.IntegerOptions) => Prompt = numbe export const map: { (f: (output: Output) => Output2): (self: Prompt) => Prompt (self: Prompt, f: (output: Output) => Output2): Prompt -} = internal.map +} = InternalPrompt.map /** * Executes the specified `Prompt`. @@ -209,14 +455,15 @@ export const map: { * @since 1.0.0 * @category execution */ -export const run: (self: Prompt) => Effect.Effect = - internal.run +export const run: (self: Prompt) => Effect = + InternalPrompt.run /** * @since 1.0.0 * @category constructors */ -export const select: (options: Prompt.SelectOptions) => Prompt = selectPrompt.select +export const select: (options: Prompt.SelectOptions) => Prompt = + InternalSelectPrompt.select /** * Creates a `Prompt` which immediately succeeds with the specified value. @@ -227,10 +474,17 @@ export const select: (options: Prompt.SelectOptions) => Prompt = selectP * @since 1.0.0 * @category constructors */ -export const succeed: (value: A) => Prompt = internal.succeed +export const succeed: (value: A) => Prompt = InternalPrompt.succeed + +/** + * @since 1.0.0 + * @category constructors + */ +export const text: (options: Prompt.TextOptions) => Prompt = InternalTextPrompt.text /** * @since 1.0.0 * @category constructors */ -export const text: (options: Prompt.TextOptions) => Prompt = textPrompt.text +export const toggle: (options: Prompt.ToggleOptions) => Prompt = + InternalTogglePrompt.toggle diff --git a/src/Prompt/Action.ts b/src/Prompt/Action.ts index 99897d8..1e7a6f9 100644 --- a/src/Prompt/Action.ts +++ b/src/Prompt/Action.ts @@ -1,13 +1,13 @@ /** * @since 1.0.0 */ -import * as internal from "../internal/prompt/action.js" +import * as InternalPromptAction from "../internal/prompt/action.js" /** * @since 1.0.0 * @category models */ -export type PromptAction = Beep | Error | NextFrame | Submit +export type PromptAction = Beep | NextFrame | Submit /** * @since 1.0.0 @@ -17,15 +17,6 @@ export interface Beep { readonly _tag: "Beep" } -/** - * @since 1.0.0 - * @category models - */ -export interface Error { - readonly _tag: "Error" - readonly message: string -} - /** * @since 1.0.0 * @category models @@ -48,22 +39,18 @@ export interface Submit { * @since 1.0.0 * @category constructors */ -export const beep: PromptAction = internal.beep - -/** - * @since 1.0.0 - * @category constructors - */ -export const error: (message: string) => PromptAction = internal.error +export const beep: PromptAction = InternalPromptAction.beep /** * @since 1.0.0 * @category constructors */ -export const nextFrame: (state: State) => PromptAction = internal.nextFrame +export const nextFrame: (state: State) => PromptAction = + InternalPromptAction.nextFrame /** * @since 1.0.0 * @category constructors */ -export const submit: (value: Output) => PromptAction = internal.submit +export const submit: (value: Output) => PromptAction = + InternalPromptAction.submit diff --git a/src/Terminal.ts b/src/Terminal.ts deleted file mode 100644 index d5fc1d7..0000000 --- a/src/Terminal.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @since 1.0.0 - */ -import type { Context, Effect, Layer } from "effect" -import * as internal from "./internal/terminal.js" - -/** - * Represents a teletype-style (TTY) terminal interface that allows for - * obtaining user input and rendering text. - * - * @since 1.0.0 - * @category models - */ -export interface Terminal { - /** - * Obtains the user's input from the terminal. - */ - readonly getUserInput: Effect.Effect - /** - * Displays the provided message to the terminal. - */ - display(message: string): Effect.Effect -} - -/** - * @since 1.0.0 - */ -export declare namespace Terminal { - /** - * Represents a user's input to a terminal. - * - * @since 1.0.0 - * @category models - */ - export interface UserInput { - readonly action: Action - readonly value: string - } - - /** - * Represents the action parsed from a user's input to a terminal. - * - * @since 1.0.0 - * @category models - */ - export type Action = - | "Backspace" - | "CursorFirst" - | "CursorLast" - | "CursorUp" - | "CursorDown" - | "CursorLeft" - | "CursorRight" - | "Delete" - | "End" - | "Exit" - | "Next" - | "NextPage" - | "PreviousPage" - | "Reset" - | "Retry" - | "Start" - | "Submit" -} - -/** - * @since 1.0.0 - * @category context - */ -export const Terminal: Context.Tag = internal.Tag - -/** - * @since 1.0.0 - * @category context - */ -export const LiveTerminal: Layer.Layer = internal.LiveTerminal diff --git a/src/index.ts b/src/index.ts index 8e644a8..8223fce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,11 +58,6 @@ export * as Span from "./HelpDoc/Span.js" */ export * as Options from "./Options.js" -/** - * @since 1.0.0 - */ -export * as Parameter from "./Parameter.js" - /** * @since 1.0.0 */ @@ -88,11 +83,6 @@ export * as RegularLanguage from "./RegularLanguage.js" */ export * as ShellType from "./ShellType.js" -/** - * @since 1.0.0 - */ -export * as Terminal from "./Terminal.js" - /** * @since 1.0.0 */ diff --git a/src/internal/args.ts b/src/internal/args.ts index 9a30546..24688f4 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -1,14 +1,16 @@ import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Terminal from "@effect/platform/Terminal" +import * as Console from "effect/Console" import * as Effect from "effect/Effect" import * as Either from "effect/Either" -import { dual } from "effect/Function" +import { dual, pipe } from "effect/Function" import * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" import * as ReadonlyArray from "effect/ReadonlyArray" +import * as Ref from "effect/Ref" import type * as Args from "../Args.js" import type * as CliConfig from "../CliConfig.js" import type * as HelpDoc from "../HelpDoc.js" -import type * as Parameter from "../Parameter.js" import type * as Primitive from "../Primitive.js" import type * as RegularLanguage from "../RegularLanguage.js" import type * as Usage from "../Usage.js" @@ -16,6 +18,7 @@ import type * as ValidationError from "../ValidationError.js" import * as InternalHelpDoc from "./helpDoc.js" import * as InternalSpan from "./helpDoc/span.js" import * as InternalPrimitive from "./primitive.js" +import * as InternalNumberPrompt from "./prompt/number.js" import * as InternalRegularLanguage from "./regularLanguage.js" import * as InternalUsage from "./usage.js" import * as InternalValidationError from "./validationError.js" @@ -31,33 +34,39 @@ const proto = { _A: (_: never) => _ } +const wizardHeader = InternalHelpDoc.p("ARGS WIZARD") + /** @internal */ export class Empty implements Args.Args { readonly [ArgsTypeId] = proto readonly _tag = "Empty" - get minSize(): number { + minSize(): number { return 0 } - get maxSize(): number { + maxSize(): number { return 0 } - get identifier(): Option.Option { + identifier(): Option.Option { return Option.none() } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { return InternalHelpDoc.empty } - get usage(): Usage.Usage { + usage(): Usage.Usage { return InternalUsage.empty } - get shortDescription(): string { - return "" + wizard(_config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return Effect.succeed(ReadonlyArray.empty()) } validate( @@ -77,7 +86,7 @@ export class Empty implements Args.Args { } /** @internal */ -export class Single implements Args.Args, Parameter.Input { +export class Single implements Args.Args { readonly [ArgsTypeId] = proto readonly _tag = "Single" @@ -87,42 +96,46 @@ export class Single implements Args.Args, Parameter.Input { readonly description: HelpDoc.HelpDoc = InternalHelpDoc.empty ) {} - get minSize(): number { + minSize(): number { return 1 } - get maxSize(): number { + maxSize(): number { return 1 } - get identifier(): Option.Option { - return Option.some(this.name) + identifier(): Option.Option { + return Option.some(this.name()) } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { return InternalHelpDoc.descriptionList([[ - InternalSpan.weak(this.name), + InternalSpan.weak(this.name()), InternalHelpDoc.sequence( - InternalHelpDoc.p(this.primitiveType.help), + InternalHelpDoc.p(this.primitiveType.help()), this.description ) ]]) } - get usage(): Usage.Usage { - return InternalUsage.named(ReadonlyArray.of(this.name), this.primitiveType.choices) - } - - get shortDescription(): string { - return `Argument $name: ${InternalSpan.getText(InternalHelpDoc.getSpan(this.description))}` + usage(): Usage.Usage { + return InternalUsage.named(ReadonlyArray.of(this.name()), this.primitiveType.choices()) } - isValid( - input: string, - config: CliConfig.CliConfig - ): Effect.Effect> { - const args = ReadonlyArray.of(input) - return this.validate(args, config).pipe(Effect.as(args)) + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const help = InternalHelpDoc.sequence(wizardHeader, this.help()) + return Console.log().pipe( + Effect.zipRight( + this.primitiveType.wizard(help).pipe(Effect.flatMap((input) => { + const args = ReadonlyArray.of(input) + return this.validate(args, config).pipe(Effect.as(args)) + })) + ) + ) } validate( @@ -144,7 +157,7 @@ export class Single implements Args.Args, Parameter.Input { }) ) } - const choices = this.primitiveType.choices + const choices = this.primitiveType.choices() if (Option.isSome(this.pseudoName) && Option.isSome(choices)) { return Effect.fail(InternalHelpDoc.p( `Missing argument <${this.pseudoName.value}> with choices ${choices.value}` @@ -157,26 +170,15 @@ export class Single implements Args.Args, Parameter.Input { } if (Option.isSome(choices)) { return Effect.fail(InternalHelpDoc.p( - `Missing argument ${this.primitiveType.typeName} with choices ${choices.value}` + `Missing argument ${this.primitiveType.typeName()} with choices ${choices.value}` )) } return Effect.fail(InternalHelpDoc.p( - `Missing argument ${this.primitiveType.typeName}` + `Missing argument ${this.primitiveType.typeName()}` )) }).pipe(Effect.mapError((help) => InternalValidationError.invalidArgument(help))) } - parse( - _args: ReadonlyArray, - _config: CliConfig.CliConfig - ): Effect.Effect< - never, - ValidationError.ValidationError, - readonly [ReadonlyArray, ReadonlyArray] - > { - return Effect.succeed([ReadonlyArray.empty(), ReadonlyArray.empty()]) - } - addDescription(description: string): Args.Args { const desc = InternalHelpDoc.sequence(this.description, InternalHelpDoc.p(description)) return new Single(this.pseudoName, this.primitiveType, desc) @@ -186,8 +188,8 @@ export class Single implements Args.Args, Parameter.Input { return pipeArguments(this, arguments) } - private get name(): string { - const name = Option.getOrElse(this.pseudoName, () => this.primitiveType.typeName) + private name(): string { + const name = Option.getOrElse(this.pseudoName, () => this.primitiveType.typeName()) return `<${name}>` } } @@ -201,32 +203,40 @@ export class Both implements Args.Args { readonly right: Args.Args ) {} - get minSize(): number { - return this.left.minSize + this.right.minSize + minSize(): number { + return this.left.minSize() + this.right.minSize() } - get maxSize(): number { - return this.left.maxSize + this.right.maxSize + maxSize(): number { + return this.left.maxSize() + this.right.maxSize() } - get identifier(): Option.Option { - const ids = ReadonlyArray.compact([this.left.identifier, this.right.identifier]) + identifier(): Option.Option { + const ids = ReadonlyArray.compact([this.left.identifier(), this.right.identifier()]) return ReadonlyArray.match(ids, { onEmpty: () => Option.none(), onNonEmpty: (ids) => Option.some(ReadonlyArray.join(ids, ", ")) }) } - get help(): HelpDoc.HelpDoc { - return InternalHelpDoc.sequence(this.left.help, this.right.help) + help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help(), this.right.help()) } - get usage(): Usage.Usage { - return InternalUsage.concat(this.left.usage, this.right.usage) + usage(): Usage.Usage { + return InternalUsage.concat(this.left.usage(), this.right.usage()) } - get shortDescription(): string { - return "" + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return Effect.zipWith( + this.left.wizard(config), + this.right.wizard(config), + (left, right) => ReadonlyArray.appendAll(left, right) + ).pipe(Effect.tap((args) => this.validate(args, config))) } validate( @@ -259,34 +269,34 @@ export class Both implements Args.Args { } /** @internal */ -export class Variadic implements Args.Args>, Parameter.Input { +export class Variadic implements Args.Args> { readonly [ArgsTypeId] = proto readonly _tag = "Variadic" constructor( - readonly value: Args.Args, + readonly args: Args.Args, readonly min: Option.Option, readonly max: Option.Option ) {} - get minSize(): number { - return Math.floor(Option.getOrElse(this.min, () => 0) * this.value.minSize) + minSize(): number { + return Math.floor(Option.getOrElse(this.min, () => 0) * this.args.minSize()) } - get maxSize(): number { + maxSize(): number { return Math.floor( - Option.getOrElse(this.max, () => Number.MAX_SAFE_INTEGER / 2) * this.value.maxSize + Option.getOrElse(this.max, () => Number.MAX_SAFE_INTEGER / 2) * this.args.maxSize() ) } - get identifier(): Option.Option { - return this.value.identifier + identifier(): Option.Option { + return this.args.identifier() } - get help(): HelpDoc.HelpDoc { - return InternalHelpDoc.mapDescriptionList(this.value.help, (oldSpan, oldBlock) => { - const min = this.minSize - const max = this.maxSize + help(): HelpDoc.HelpDoc { + return InternalHelpDoc.mapDescriptionList(this.args.help(), (oldSpan, oldBlock) => { + const min = this.minSize() + const max = this.maxSize() const newSpan = InternalSpan.text( Option.isSome(this.max) ? ` ${min} - ${max}` : min === 0 ? "..." : ` ${min}+` ) @@ -301,12 +311,40 @@ export class Variadic implements Args.Args>, Parameter.Input }) } - get usage(): Usage.Usage { - return InternalUsage.repeated(this.value.usage) + usage(): Usage.Usage { + return InternalUsage.repeated(this.args.usage()) } - get shortDescription(): string { - return InternalHelpDoc.toAnsiText(this.help) + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const repeatHelp = InternalHelpDoc.p("How many times should this argument should be repeated?") + const message = pipe( + wizardHeader, + InternalHelpDoc.sequence(this.help()), + InternalHelpDoc.sequence(repeatHelp) + ) + return Console.log().pipe( + Effect.zipRight(InternalNumberPrompt.integer({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + min: this.minSize(), + max: this.maxSize() + })), + Effect.flatMap((n) => + Ref.make(ReadonlyArray.empty()).pipe( + Effect.flatMap((ref) => + this.args.wizard(config).pipe( + Effect.flatMap((args) => Ref.update(ref, ReadonlyArray.appendAll(args))), + Effect.repeatN(n - 1), + Effect.zipRight(Ref.get(ref)), + Effect.tap((args) => this.validate(args, config)) + ) + ) + ) + ) + ) } validate( @@ -330,7 +368,7 @@ export class Variadic implements Args.Args>, Parameter.Input if (acc.length >= max1) { return Effect.succeed([args, acc]) } - return this.value.validate(args, config).pipe(Effect.matchEffect({ + return this.args.validate(args, config).pipe(Effect.matchEffect({ onFailure: (failure) => acc.length >= min1 && ReadonlyArray.isEmptyReadonlyArray(args) ? Effect.succeed([args, acc]) @@ -343,27 +381,8 @@ export class Variadic implements Args.Args>, Parameter.Input ) } - parse( - _args: ReadonlyArray, - _config: CliConfig.CliConfig - ): Effect.Effect< - never, - ValidationError.ValidationError, - readonly [ReadonlyArray, ReadonlyArray] - > { - return Effect.succeed([ReadonlyArray.empty(), ReadonlyArray.empty()]) - } - - isValid( - input: string, - config: CliConfig.CliConfig - ): Effect.Effect> { - const args = input.split(" ") - return this.validate(args, config).pipe(Effect.as(args)) - } - addDescription(description: string): Args.Args> { - return new Variadic(this.value.addDescription(description), this.min, this.max) + return new Variadic(this.args.addDescription(description), this.min, this.max) } pipe() { @@ -381,28 +400,32 @@ export class Map implements Args.Args { readonly f: (value: A) => Either.Either ) {} - get minSize(): number { - return this.args.minSize + minSize(): number { + return this.args.minSize() } - get maxSize(): number { - return this.args.maxSize + maxSize(): number { + return this.args.maxSize() } - get identifier(): Option.Option { - return this.args.identifier + identifier(): Option.Option { + return this.args.identifier() } - get help(): HelpDoc.HelpDoc { - return this.args.help + help(): HelpDoc.HelpDoc { + return this.args.help() } - get usage(): Usage.Usage { - return this.args.usage + usage(): Usage.Usage { + return this.args.usage() } - get shortDescription(): string { - return this.args.shortDescription + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return this.args.wizard(config).pipe(Effect.tap((args) => this.validate(args, config))) } validate( @@ -626,7 +649,7 @@ export const repeatedAtLeastOnce = ( if (ReadonlyArray.isNonEmptyReadonlyArray(values)) { return values } - const message = Option.match(self.identifier, { + const message = Option.match(self.identifier(), { onNone: () => "An anonymous variadic argument", onSome: (identifier) => `The variadic option '${identifier}' ` }) @@ -650,7 +673,7 @@ export const toRegularLanguage = ( ) } if (isVariadic(self)) { - return InternalRegularLanguage.repeated(toRegularLanguage(self.value), { + return InternalRegularLanguage.repeated(toRegularLanguage(self.args), { min: Option.getOrUndefined(self.min), max: Option.getOrUndefined(self.max) }) diff --git a/src/internal/builtInOptions.ts b/src/internal/builtInOptions.ts index 7523f9d..1a4c6e8 100644 --- a/src/internal/builtInOptions.ts +++ b/src/internal/builtInOptions.ts @@ -39,9 +39,9 @@ export const showHelp = ( }) /** @internal */ -export const showWizard = (commmand: Command.Command): BuiltInOptions.BuiltInOptions => ({ +export const showWizard = (command: Command.Command): BuiltInOptions.BuiltInOptions => ({ _tag: "ShowWizard", - commmand + command }) /** @internal */ diff --git a/src/internal/cliApp.ts b/src/internal/cliApp.ts index 73255e8..30fdfc6 100644 --- a/src/internal/cliApp.ts +++ b/src/internal/cliApp.ts @@ -1,15 +1,13 @@ -import type * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" +import type * as Terminal from "@effect/platform/Terminal" import * as Console from "effect/Console" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import { dual, pipe } from "effect/Function" -import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Order from "effect/Order" import * as ReadonlyArray from "effect/ReadonlyArray" import * as ReadonlyRecord from "effect/ReadonlyRecord" -import { unify } from "effect/Unify" import type * as BuiltInOptions from "../BuiltInOptions.js" import type * as CliApp from "../CliApp.js" import type * as CliConfig from "../CliConfig.js" @@ -24,7 +22,6 @@ import * as InternalCompgen from "./compgen.js" import * as InternalCompletion from "./completion.js" import * as InternalHelpDoc from "./helpDoc.js" import * as InternalSpan from "./helpDoc/span.js" -import * as InternalTerminal from "./terminal.js" import * as InternalUsage from "./usage.js" import * as InternalValidationError from "./validationError.js" @@ -50,8 +47,6 @@ export const make = (config: { // Combinators // ============================================================================= -const MainLive = Layer.merge(InternalTerminal.LiveTerminal, InternalCompgen.LiveCompgen) - /** @internal */ export const run = dual< ( @@ -82,7 +77,7 @@ export const run = dual< // Handle the command return Effect.matchEffect(self.command.parse(prefixedArgs, config), { onFailure: (e) => Effect.zipRight(printDocs(e.error), Effect.fail(e)), - onSuccess: unify((directive) => { + onSuccess: Effect.unifiedFn((directive) => { switch (directive._tag) { case "UserDefined": { return execute(directive.value) @@ -99,7 +94,7 @@ export const run = dual< } }) }) - }).pipe(Effect.provide(MainLive))) + }).pipe(Effect.provideServiceEffect(InternalCompgen.Tag, InternalCompgen.make(Option.none())))) // ============================================================================= // Internals @@ -113,43 +108,42 @@ const handleBuiltInOption = ( builtIn: BuiltInOptions.BuiltInOptions, config: CliConfig.CliConfig ): Effect.Effect< - Compgen.Compgen | FileSystem.FileSystem | Path.Path, + CliApp.CliApp.Environment | Compgen.Compgen | Terminal.Terminal, ValidationError.ValidationError, void -> => - Effect.gen(function*(_) { - switch (builtIn._tag) { - case "ShowHelp": { - const banner = InternalHelpDoc.h1(InternalSpan.code(self.name)) - const header = InternalHelpDoc.p(InternalSpan.concat( - InternalSpan.text(`${self.name} ${self.version} -- `), - self.summary - )) - const usage = InternalHelpDoc.sequence( - InternalHelpDoc.h1("USAGE"), - pipe( - InternalUsage.enumerate(builtIn.usage, config), - ReadonlyArray.map((span) => - InternalHelpDoc.p(InternalSpan.concat(InternalSpan.text("$ "), span)) - ), - ReadonlyArray.reduceRight( - InternalHelpDoc.empty, - (left, right) => InternalHelpDoc.sequence(left, right) - ) +> => { + switch (builtIn._tag) { + case "ShowHelp": { + const banner = InternalHelpDoc.h1(InternalSpan.code(self.name)) + const header = InternalHelpDoc.p(InternalSpan.concat( + InternalSpan.text(`${self.name} ${self.version} -- `), + self.summary + )) + const usage = InternalHelpDoc.sequence( + InternalHelpDoc.h1("USAGE"), + pipe( + InternalUsage.enumerate(builtIn.usage, config), + ReadonlyArray.map((span) => + InternalHelpDoc.p(InternalSpan.concat(InternalSpan.text("$ "), span)) + ), + ReadonlyArray.reduceRight( + InternalHelpDoc.empty, + (left, right) => InternalHelpDoc.sequence(left, right) ) ) - const helpDoc = pipe( - banner, - InternalHelpDoc.sequence(header), - InternalHelpDoc.sequence(usage), - InternalHelpDoc.sequence(builtIn.helpDoc), - InternalHelpDoc.sequence(self.footer) - ) - return yield* _(Console.log(InternalHelpDoc.toAnsiText(helpDoc))) - } - case "ShowCompletionScript": { - const path = yield* _(Path.Path) - const commandNames = ReadonlyArray.fromIterable(self.command.names) + ) + const helpDoc = pipe( + banner, + InternalHelpDoc.sequence(header), + InternalHelpDoc.sequence(usage), + InternalHelpDoc.sequence(builtIn.helpDoc), + InternalHelpDoc.sequence(self.footer) + ) + return Console.log(InternalHelpDoc.toAnsiText(helpDoc)) + } + case "ShowCompletionScript": { + return Effect.flatMap(Path.Path, (path) => { + const commandNames = ReadonlyArray.fromIterable(self.command.names()) const programNames = ReadonlyArray.isNonEmptyReadonlyArray(commandNames) ? commandNames : ReadonlyArray.of(self.name) @@ -159,11 +153,14 @@ const handleBuiltInOption = ( builtIn.shellType, path ) - return yield* _(Console.log(script)) - } - case "ShowCompletions": { - const compgen = yield* _(InternalCompgen.Tag) - const env = yield* _(Effect.sync(() => process.env)) + return Console.log(script) + }) + } + case "ShowCompletions": { + return Effect.all([ + InternalCompgen.Tag, + Effect.sync(() => globalThis.process.env) + ]).pipe(Effect.flatMap(([compgen, env]) => { const tupleOrder = Order.mapInput(Order.number, (tuple: [number, string]) => tuple[0]) const compWords = pipe( ReadonlyRecord.collect( @@ -177,20 +174,59 @@ const handleBuiltInOption = ( ReadonlyArray.sortBy(tupleOrder), ReadonlyArray.map(([, value]) => value) ) - const completions = yield* _(InternalCompletion.getCompletions( + return InternalCompletion.getCompletions( compWords, builtIn.index, self.command, config, compgen - )) - return Effect.forEach(completions, (word) => Console.log(word), { discard: true }) - } - case "ShowWizard": { - return yield* _(Console.log("Showing the wizard")) - } + ).pipe( + Effect.flatMap((completions) => + Effect.forEach(completions, (word) => Console.log(word), { discard: true }) + ) + ) + })) + } + case "ShowWizard": { + const summary = InternalSpan.isEmpty(self.summary) + ? InternalSpan.empty + : InternalSpan.spans([ + InternalSpan.space, + InternalSpan.text("--"), + InternalSpan.space, + self.summary + ]) + const instructions = InternalHelpDoc.sequence( + InternalHelpDoc.p(InternalSpan.spans([ + InternalSpan.text("The wizard mode will assist you with constructing commands for"), + InternalSpan.space, + InternalSpan.code(`${self.name} (${self.version})`), + InternalSpan.text(".") + ])), + InternalHelpDoc.p("Please answer all prompts provided by the wizard.") + ) + const description = InternalHelpDoc.descriptionList([[ + InternalSpan.text("Instructions"), + instructions + ]]) + const header = InternalHelpDoc.h1( + InternalSpan.spans([ + InternalSpan.code("Wizard Mode for CLI Application:"), + InternalSpan.space, + InternalSpan.code(self.name), + InternalSpan.space, + InternalSpan.code(`(${self.version})`), + summary + ]) + ) + const help = InternalHelpDoc.sequence(header, description) + return Console.log(InternalHelpDoc.toAnsiText(help)).pipe( + Effect.zipRight(builtIn.command.wizard(config)), + Effect.tap((args) => Console.log(InternalHelpDoc.toAnsiText(renderWizardArgs(args)))) + ) } - }) + } +} const prefixCommand = (self: Command.Command): ReadonlyArray => { let command: Command.Command | undefined = self @@ -213,3 +249,19 @@ const prefixCommand = (self: Command.Command): ReadonlyArray => { } return prefix } + +const renderWizardArgs = (args: ReadonlyArray) => { + const params = pipe( + ReadonlyArray.filter(args, (param) => param.length > 0), + ReadonlyArray.join(" ") + ) + const executeMsg = InternalSpan.weak( + "You may now execute your command directly with the following options and arguments:" + ) + return InternalHelpDoc.blocks([ + InternalHelpDoc.p(""), + InternalHelpDoc.p(InternalSpan.strong(InternalSpan.code("Wizard Mode Complete!"))), + InternalHelpDoc.p(executeMsg), + InternalHelpDoc.p(InternalSpan.code(` ${params}`)) + ]) +} diff --git a/src/internal/command.ts b/src/internal/command.ts index 8d3db84..62049fc 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -1,4 +1,6 @@ import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Terminal from "@effect/platform/Terminal" +import * as Console from "effect/Console" import * as Effect from "effect/Effect" import * as Either from "effect/Either" import { dual, pipe } from "effect/Function" @@ -16,7 +18,6 @@ import type * as Span from "../HelpDoc/Span.js" import type * as Options from "../Options.js" import type * as Prompt from "../Prompt.js" import type * as RegularLanguage from "../RegularLanguage.js" -import type * as Terminal from "../Terminal.js" import type * as Usage from "../Usage.js" import type * as ValidationError from "../ValidationError.js" import * as InternalArgs from "./args.js" @@ -27,6 +28,7 @@ import * as InternalHelpDoc from "./helpDoc.js" import * as InternalSpan from "./helpDoc/span.js" import * as InternalOptions from "./options.js" import * as InternalPrompt from "./prompt.js" +import * as InternalSelectPrompt from "./prompt/select.js" import * as InternalRegularLanguage from "./regularLanguage.js" import * as InternalUsage from "./usage.js" import * as InternalValidationError from "./validationError.js" @@ -56,38 +58,52 @@ export class Standard readonly args: Args.Args ) {} - get names(): HashSet.HashSet { + names(): HashSet.HashSet { return HashSet.make(this.name) } - get subcommands(): HashMap.HashMap> { + subcommands(): HashMap.HashMap> { return HashMap.make([this.name, this]) } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { const header = InternalHelpDoc.isEmpty(this.description) ? InternalHelpDoc.empty : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), this.description) - const argsHelp = this.args.help + const argsHelp = this.args.help() const argsSection = InternalHelpDoc.isEmpty(argsHelp) ? InternalHelpDoc.empty : InternalHelpDoc.sequence(InternalHelpDoc.h1("ARGUMENTS"), argsHelp) - const optionsHelp = this.options.help + const optionsHelp = this.options.help() const optionsSection = InternalHelpDoc.isEmpty(optionsHelp) ? InternalHelpDoc.empty : InternalHelpDoc.sequence(InternalHelpDoc.h1("OPTIONS"), optionsHelp) return InternalHelpDoc.sequence(header, InternalHelpDoc.sequence(argsSection, optionsSection)) } - get usage(): Usage.Usage { + usage(): Usage.Usage { return InternalUsage.concat( InternalUsage.named(ReadonlyArray.of(this.name), Option.none()), - InternalUsage.concat(this.options.usage, this.args.usage) + InternalUsage.concat(this.options.usage(), this.args.usage()) ) } - get shortDescription(): string { - return InternalSpan.getText(InternalHelpDoc.getSpan(this.help)) + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const message = InternalHelpDoc.p(pipe( + InternalSpan.text("\n\n"), + InternalSpan.concat(InternalSpan.strong(InternalSpan.code("COMMAND:"))), + InternalSpan.concat(InternalSpan.space), + InternalSpan.concat(InternalSpan.code(this.name)) + )) + return Console.log(InternalHelpDoc.toAnsiText(message)).pipe(Effect.zipRight(Effect.zipWith( + this.options.wizard(config), + this.args.wizard(config), + (options, args) => ReadonlyArray.prepend(ReadonlyArray.appendAll(options, args), this.name) + ))) } parse( @@ -132,7 +148,9 @@ export class Standard const normalizedArgv0 = InternalCliConfig.normalizeCase(config, argv0) const normalizedCommandName = InternalCliConfig.normalizeCase(config, this.name) if (normalizedArgv0 === normalizedCommandName) { - const options = InternalBuiltInOptions.builtInOptions(this, this.usage, this.help) + const help = this.help() + const usage = this.usage() + const options = InternalBuiltInOptions.builtInOptions(this, usage, help) return InternalOptions.validate(options, ReadonlyArray.drop(args, 1), config).pipe( Effect.flatMap((tuple) => tuple[2]), Effect.catchTag("NoSuchElementException", () => { @@ -188,7 +206,7 @@ export class Standard if (ReadonlyArray.contains(args, "--help") || ReadonlyArray.contains(args, "-h")) { return parseBuiltInArgs(ReadonlyArray.make(this.name, "--help")) } - if (ReadonlyArray.contains(args, "--wizard") || ReadonlyArray.contains(args, "-w")) { + if (ReadonlyArray.contains(args, "--wizard")) { return parseBuiltInArgs(ReadonlyArray.make(this.name, "--wizard")) } const error = InternalHelpDoc.p(`Missing command name: '${this.name}'`) @@ -233,26 +251,30 @@ export class GetUserInput readonly prompt: Prompt.Prompt ) {} - get names(): HashSet.HashSet { + names(): HashSet.HashSet { return HashSet.make(this.name) } - get subcommands(): HashMap.HashMap> { + subcommands(): HashMap.HashMap> { return HashMap.make([this.name, this]) } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { const header = InternalHelpDoc.h1("DESCRIPTION") const content = InternalHelpDoc.p("This command will prompt the user for information") return InternalHelpDoc.sequence(header, content) } - get usage(): Usage.Usage { + usage(): Usage.Usage { return InternalUsage.named(ReadonlyArray.of(this.name), Option.none()) } - get shortDescription(): string { - return "" + wizard(_config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return Effect.succeed(ReadonlyArray.empty()) } parse( @@ -288,24 +310,28 @@ export class Map implements Command.Command { readonly f: (value: A) => B ) {} - get names(): HashSet.HashSet { - return this.command.names + names(): HashSet.HashSet { + return this.command.names() } - get subcommands(): HashMap.HashMap> { - return this.command.subcommands + subcommands(): HashMap.HashMap> { + return this.command.subcommands() } - get help(): HelpDoc.HelpDoc { - return this.command.help + help(): HelpDoc.HelpDoc { + return this.command.help() } - get usage(): Usage.Usage { - return this.command.usage + usage(): Usage.Usage { + return this.command.usage() } - get shortDescription(): string { - return this.command.shortDescription + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return this.command.wizard(config) } parse( @@ -336,24 +362,51 @@ export class OrElse implements Command.Command { readonly right: Command.Command ) {} - get names(): HashSet.HashSet { - return HashSet.union(this.left.names, this.right.names) + names(): HashSet.HashSet { + return HashSet.union(this.left.names(), this.right.names()) } - get subcommands(): HashMap.HashMap> { - return HashMap.union(this.left.subcommands, this.right.subcommands) + subcommands(): HashMap.HashMap> { + return HashMap.union(this.left.subcommands(), this.right.subcommands()) } - get help(): HelpDoc.HelpDoc { - return InternalHelpDoc.sequence(this.left.help, this.right.help) + help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help(), this.right.help()) } - get usage(): Usage.Usage { + usage(): Usage.Usage { return InternalUsage.mixed } - get shortDescription(): string { - return "" + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const message = InternalHelpDoc.p("Select which command you would like to execute") + const makeChoice = (title: string, value: Command.Command) => ({ + title, + value: [title, value] as const + }) + const choices = ReadonlyArray.compact([ + Option.map( + ReadonlyArray.head(Array.from(this.left.names())), + (title) => makeChoice(title, this.left) + ), + Option.map( + ReadonlyArray.head(Array.from(this.right.names())), + (title) => makeChoice(title, this.right) + ) + ]) + return Console.log().pipe( + Effect.zipRight(InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices + })), + Effect.flatMap(([name, command]) => + command.wizard(config).pipe(Effect.map(ReadonlyArray.prepend(name))) + ) + ) } parse( @@ -390,21 +443,21 @@ export class Subcommands, B extends Command.Comma readonly child: B ) {} - get names(): HashSet.HashSet { - return this.parent.names + names(): HashSet.HashSet { + return this.parent.names() } - get subcommands(): HashMap.HashMap> { - return this.child.subcommands + subcommands(): HashMap.HashMap> { + return this.child.subcommands() } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { const getUsage = ( command: Command.Command, preceding: ReadonlyArray ): ReadonlyArray<[Span.Span, Span.Span]> => { - if (isStandard(command)) { - const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(command.usage)) + if (isStandard(command) || isGetUserInput(command)) { + const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(command.usage())) const usages = ReadonlyArray.append(preceding, usage) const finalUsage = ReadonlyArray.reduceRight( usages, @@ -416,9 +469,11 @@ export class Subcommands, B extends Command.Comma ? acc : InternalSpan.concat(acc, InternalSpan.concat(InternalSpan.space, next)) ) - return ReadonlyArray.of([finalUsage, InternalHelpDoc.getSpan(command.description)]) + const description = isStandard(command) + ? InternalHelpDoc.getSpan(command.description) + : InternalSpan.empty + return ReadonlyArray.of([finalUsage, description]) } - // TODO: if (isPrompt(command)) {} if (isMap(command)) { return getUsage(command.command, preceding) } @@ -463,7 +518,7 @@ export class Subcommands, B extends Command.Comma throw new Error("[BUG]: Subcommands.usage - received empty list of subcommands to print") } return InternalHelpDoc.sequence( - this.parent.help, + this.parent.help(), InternalHelpDoc.sequence( InternalHelpDoc.h1("COMMANDS"), printSubcommands(getUsage(this.child, ReadonlyArray.empty())) @@ -471,12 +526,34 @@ export class Subcommands, B extends Command.Comma ) } - get usage(): Usage.Usage { - return InternalUsage.concat(this.parent.usage, this.child.usage) + usage(): Usage.Usage { + return InternalUsage.concat(this.parent.usage(), this.child.usage()) } - get shortDescription(): string { - return this.parent.shortDescription + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const message = InternalHelpDoc.p("Select which command you would like to execute") + const makeChoice = (title: string, value: Command.Command) => ({ title, value }) + const parentName = Option.getOrElse( + ReadonlyArray.head(Array.from(this.names())), + () => "" + ) + const parentChoice = makeChoice(parentName, this.parent) + const childChoices = ReadonlyArray.map( + Array.from(this.subcommands()), + ([name, command]) => makeChoice(name, command) + ) + const choices = ReadonlyArray.prepend(childChoices, parentChoice) + return Console.log().pipe( + Effect.zipRight(InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices + })), + Effect.flatMap((command) => command.wizard(config)) + ) } parse( @@ -487,16 +564,22 @@ export class Subcommands, B extends Command.Comma ValidationError.ValidationError, CommandDirective.CommandDirective> > { + const names = Array.from(this.names()) + const subcommands = this.subcommands() + const [parentArgs, childArgs] = ReadonlyArray.span( + args, + (name) => !HashMap.has(subcommands, name) + ) const helpDirectiveForParent = Effect.succeed( - InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp(this.usage, this.help)) + InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp(this.usage(), this.help())) ) - const helpDirectiveForChild = this.child.parse(args.slice(1), config).pipe( + const helpDirectiveForChild = this.child.parse(childArgs, config).pipe( Effect.flatMap((directive) => { if ( InternalCommandDirective.isBuiltIn(directive) && InternalBuiltInOptions.isShowHelp(directive.option) ) { - const parentName = Option.getOrElse(ReadonlyArray.head(Array.from(this.names)), () => "") + const parentName = Option.getOrElse(ReadonlyArray.head(names), () => "") const newDirective = InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( InternalUsage.concat( InternalUsage.named(ReadonlyArray.of(parentName), Option.none()), @@ -512,7 +595,7 @@ export class Subcommands, B extends Command.Comma const wizardDirectiveForParent = Effect.succeed( InternalCommandDirective.builtIn(InternalBuiltInOptions.showWizard(this)) ) - const wizardDirectiveForChild = this.child.parse(args.slice(1), config).pipe( + const wizardDirectiveForChild = this.child.parse(childArgs, config).pipe( Effect.flatMap((directive) => { if ( InternalCommandDirective.isBuiltIn(directive) && @@ -523,11 +606,6 @@ export class Subcommands, B extends Command.Comma return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) }) ) - const subcommands = this.subcommands - const [parentArgs, childArgs] = ReadonlyArray.span( - args, - (name) => !HashMap.has(subcommands, name) - ) return this.parent.parse(parentArgs, config).pipe( Effect.flatMap((directive) => { switch (directive._tag) { @@ -546,12 +624,9 @@ export class Subcommands, B extends Command.Comma return this.child.parse(args, config).pipe(Effect.mapBoth({ onFailure: (err) => { if (InternalValidationError.isCommandMismatch(err)) { - const parentName = Option.getOrElse( - ReadonlyArray.head(Array.from(this.names)), - () => "" - ) + const parentName = Option.getOrElse(ReadonlyArray.head(names), () => "") const subcommandNames = pipe( - ReadonlyArray.fromIterable(HashMap.keys(this.subcommands)), + ReadonlyArray.fromIterable(HashMap.keys(this.subcommands())), ReadonlyArray.map((name) => `'${name}'`) ) const oneOf = subcommandNames.length === 1 ? "" : " one of" diff --git a/src/internal/compgen.ts b/src/internal/compgen.ts index 45fd8fa..c55335b 100644 --- a/src/internal/compgen.ts +++ b/src/internal/compgen.ts @@ -77,10 +77,10 @@ export const make = (workingDirectory: Option.Option): Effect.Effect< export const Tag = Context.Tag() /** @internal */ -export const LiveCompgen: Layer.Layer = - Layer.effect(Tag, make(Option.none())) +export const layer: Layer.Layer = Layer + .effect(Tag, make(Option.none())) -export const TestCompgen = ( +export const testLayer = ( workingDirectory: string ): Layer.Layer => Layer.effect(Tag, make(Option.some(workingDirectory))) diff --git a/src/internal/helpDoc.ts b/src/internal/helpDoc.ts index 392e55d..e62e977 100644 --- a/src/internal/helpDoc.ts +++ b/src/internal/helpDoc.ts @@ -3,7 +3,7 @@ import * as AnsiRender from "@effect/printer-ansi/AnsiRender" import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" import * as Doc from "@effect/printer/Doc" import * as Optimize from "@effect/printer/Optimize" -import { dual } from "effect/Function" +import { dual, pipe } from "effect/Function" import * as ReadonlyArray from "effect/ReadonlyArray" import type * as HelpDoc from "../HelpDoc.js" import type * as Span from "../HelpDoc/Span.js" @@ -132,42 +132,54 @@ export const mapDescriptionList = dual< ? descriptionList(ReadonlyArray.map(self.definitions, ([span, helpDoc]) => f(span, helpDoc))) : self) -const helpDocToAnsiDoc: { - [K in HelpDoc.HelpDoc["_tag"]]: (self: Extract) => AnsiDoc.AnsiDoc -} = { - Empty: () => Doc.empty, - Paragraph: (self) => Doc.cat(InternalSpan.toAnsiDoc(self.value), Doc.hardLine), - Header: (self) => - Doc.cat(Doc.annotate(InternalSpan.toAnsiDoc(self.value), AnsiStyle.bold), Doc.hardLine), - Enumeration: (self) => - Doc.indent( - Doc.vsep(self.elements.map((doc) => - Doc.cat( - Doc.text("- "), - helpDocToAnsiDoc[doc._tag](doc as any) - ) - )), - 2 - ), - DescriptionList: (self) => - Doc.vsep(self.definitions.map(([s, d]) => - Doc.cats([ - Doc.annotate(InternalSpan.toAnsiDoc(s), AnsiStyle.bold), - Doc.empty, - Doc.indent(helpDocToAnsiDoc[d._tag](d as any), 2) - ]) - )), - Sequence: (self) => - Doc.vsep([ - helpDocToAnsiDoc[self.left._tag](self.left as any), - helpDocToAnsiDoc[self.right._tag](self.right as any) - ]) -} - /** @internal */ export const toAnsiDoc = (self: HelpDoc.HelpDoc): AnsiDoc.AnsiDoc => - Optimize.optimize(helpDocToAnsiDoc[self._tag](self as any), Optimize.Deep) + Optimize.optimize(toAnsiDocInternal(self), Optimize.Deep) /** @internal */ export const toAnsiText = (self: HelpDoc.HelpDoc): string => AnsiRender.prettyDefault(toAnsiDoc(self)) + +// ============================================================================= +// Internals +// ============================================================================= + +const toAnsiDocInternal = (self: HelpDoc.HelpDoc): AnsiDoc.AnsiDoc => { + switch (self._tag) { + case "Empty": { + return Doc.empty + } + case "Header": { + return pipe( + Doc.annotate(InternalSpan.toAnsiDoc(self.value), AnsiStyle.bold), + Doc.cat(Doc.hardLine) + ) + } + case "Paragraph": { + return pipe( + InternalSpan.toAnsiDoc(self.value), + Doc.cat(Doc.hardLine) + ) + } + case "DescriptionList": { + const definitions = self.definitions.map(([span, doc]) => + Doc.cats([ + Doc.annotate(InternalSpan.toAnsiDoc(span), AnsiStyle.bold), + Doc.empty, + Doc.indent(toAnsiDocInternal(doc), 2) + ]) + ) + return Doc.vsep(definitions) + } + case "Enumeration": { + const elements = self.elements.map((doc) => Doc.cat(Doc.text("- "), toAnsiDocInternal(doc))) + return Doc.indent(Doc.vsep(elements), 2) + } + case "Sequence": { + return Doc.vsep([ + toAnsiDocInternal(self.left), + toAnsiDocInternal(self.right) + ]) + } + } +} diff --git a/src/internal/options.ts b/src/internal/options.ts index 59c5eed..f98b079 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -1,5 +1,6 @@ import type * as FileSystem from "@effect/platform/FileSystem" -import * as Schema from "@effect/schema/Schema" +import type * as Terminal from "@effect/platform/Terminal" +import * as Console from "effect/Console" import * as Effect from "effect/Effect" import * as Either from "effect/Either" import { dual, pipe } from "effect/Function" @@ -11,7 +12,6 @@ import * as ReadonlyArray from "effect/ReadonlyArray" import type * as CliConfig from "../CliConfig.js" import type * as HelpDoc from "../HelpDoc.js" import type * as Options from "../Options.js" -import type * as Parameter from "../Parameter.js" import type * as Primitive from "../Primitive.js" import type * as RegularLanguage from "../RegularLanguage.js" import type * as Usage from "../Usage.js" @@ -19,8 +19,9 @@ import type * as ValidationError from "../ValidationError.js" import * as InternalAutoCorrect from "./autoCorrect.js" import * as InternalCliConfig from "./cliConfig.js" import * as InternalHelpDoc from "./helpDoc.js" -import * as InternalSpan from "./helpDoc/span.js" import * as InternalPrimitive from "./primitive.js" +import * as InternalListPrompt from "./prompt/list.js" +import * as InternalSelectPrompt from "./prompt/select.js" import * as InternalRegularLanguage from "./regularLanguage.js" import * as InternalUsage from "./usage.js" import * as InternalValidationError from "./validationError.js" @@ -36,35 +37,41 @@ const proto = { _A: (_: never) => _ } +const wizardHeader = InternalHelpDoc.p("OPTIONS WIZARD") + /** @internal */ export class Empty implements Options.Options { readonly [OptionsTypeId] = proto readonly _tag = "Empty" - get identifier(): Option.Option { + identifier(): Option.Option { return Option.none() } - get flattened(): ReadonlyArray { + flattened(): ReadonlyArray> { return ReadonlyArray.empty() } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { return InternalHelpDoc.empty } - get usage(): Usage.Usage { + usage(): Usage.Usage { return InternalUsage.empty } - get shortDescription(): string { - return "" - } - modifySingle(_f: <_>(single: Single<_>) => Single<_>): Options.Options { return new Empty() } + wizard(_config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return Effect.succeed(ReadonlyArray.empty()) + } + validate( _args: HashMap.HashMap>, _config: CliConfig.CliConfig @@ -78,7 +85,7 @@ export class Empty implements Options.Options { } /** @internal */ -export class Single implements Options.Options, Parameter.Input { +export class Single implements Options.Options.ParseableOptions { readonly [OptionsTypeId] = proto readonly _tag = "Single" @@ -90,54 +97,57 @@ export class Single implements Options.Options, Parameter.Input { readonly pseudoName: Option.Option = Option.none() ) {} - get identifier(): Option.Option { - return Option.some(this.fullName) + identifier(): Option.Option { + return Option.some(this.fullName()) } - get flattened(): ReadonlyArray { + flattened(): ReadonlyArray> { return ReadonlyArray.of(this) } - get help(): HelpDoc.HelpDoc { + help(): HelpDoc.HelpDoc { return InternalHelpDoc.descriptionList(ReadonlyArray.of([ - InternalHelpDoc.getSpan(InternalUsage.getHelp(this.usage)), - InternalHelpDoc.sequence(InternalHelpDoc.p(this.primitiveType.help), this.description) + InternalHelpDoc.getSpan(InternalUsage.getHelp(this.usage())), + InternalHelpDoc.sequence(InternalHelpDoc.p(this.primitiveType.help()), this.description) ])) } - get usage(): Usage.Usage { + usage(): Usage.Usage { const acceptedValues = InternalPrimitive.isBool(this.primitiveType) ? Option.none() - : Option.orElse(this.primitiveType.choices, () => Option.some(this.placeholder)) - return InternalUsage.named(this.names, acceptedValues) + : Option.orElse(this.primitiveType.choices(), () => Option.some(this.placeholder())) + return InternalUsage.named(this.names(), acceptedValues) } - get names(): ReadonlyArray { + names(): ReadonlyArray.NonEmptyArray { + const order = Order.mapInput(Order.boolean, (tuple: readonly [boolean, string]) => !tuple[0]) return pipe( ReadonlyArray.prepend(this.aliases, this.name), ReadonlyArray.map((str) => this.makeFullName(str)), - ReadonlyArray.sort( - Order.mapInput(Order.boolean, (tuple: readonly [boolean, string]) => !tuple[0]) - ), + ReadonlyArray.sortNonEmpty(order), ReadonlyArray.map((tuple) => tuple[1]) ) } - get shortDescription(): string { - return `Option ${this.name}. ${InternalSpan.getText(InternalHelpDoc.getSpan(this.description))}` - } - modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { return f(this) } - isValid( - input: string, - config: CliConfig.CliConfig - ): Effect.Effect> { - // There will always be at least one name in names - const args = ReadonlyArray.make(this.names[0]!, input) - return this.parse(args, config).pipe(Effect.as(args)) + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const help = InternalHelpDoc.sequence(wizardHeader, this.help()) + return Console.log().pipe( + Effect.zipRight( + this.primitiveType.wizard(help).pipe(Effect.flatMap((input) => { + // There will always be at least one name in names + const args = ReadonlyArray.make(this.names()[0]!, input) + return this.parse(args, config).pipe(Effect.as(args)) + })) + ) + ) } parse( @@ -155,7 +165,7 @@ export class Single implements Options.Options, Parameter.Input { const tail = ReadonlyArray.tailNonEmpty(args) const normalizedArgv0 = InternalCliConfig.normalizeCase(config, head) const normalizedNames = ReadonlyArray.map( - this.names, + this.names(), (name) => InternalCliConfig.normalizeCase(config, name) ) if (ReadonlyArray.contains(normalizedNames, normalizedArgv0)) { @@ -180,10 +190,12 @@ export class Single implements Options.Options, Parameter.Input { ReadonlyArray.drop(tail, 1) ]) } - const error = InternalHelpDoc.p(`Expected a value following option: '${this.fullName}'`) + const error = InternalHelpDoc.p( + `Expected a value following option: '${this.fullName()}'` + ) return Effect.fail(InternalValidationError.missingValue(error)) } - const fullName = this.fullName + const fullName = this.fullName() if ( this.name.length > config.autoCorrectLimit + 1 && InternalAutoCorrect.levensteinDistance(head, fullName, config) <= @@ -197,7 +209,7 @@ export class Single implements Options.Options, Parameter.Input { const error = InternalHelpDoc.p(`Expected to find option: '${fullName}'`) return Effect.fail(InternalValidationError.missingFlag(error)) } - const error = InternalHelpDoc.p(`Expected to find option: '${this.fullName}'`) + const error = InternalHelpDoc.p(`Expected to find option: '${this.fullName()}'`) return Effect.fail(InternalValidationError.missingFlag(error)) }) ) @@ -207,7 +219,7 @@ export class Single implements Options.Options, Parameter.Input { args: HashMap.HashMap>, config: CliConfig.CliConfig ): Effect.Effect { - const names = ReadonlyArray.filterMap(this.names, (name) => HashMap.get(args, name)) + const names = ReadonlyArray.filterMap(this.names(), (name) => HashMap.get(args, name)) if (ReadonlyArray.isNonEmptyReadonlyArray(names)) { const head = ReadonlyArray.headNonEmpty(names) const tail = ReadonlyArray.tailNonEmpty(names) @@ -229,11 +241,11 @@ export class Single implements Options.Options, Parameter.Input { return Effect.fail(InternalValidationError.keyValuesDetected(InternalHelpDoc.empty, head)) } const error = InternalHelpDoc.p( - `More than one reference to option '${this.fullName}' detected` + `More than one reference to option '${this.fullName()}' detected` ) return Effect.fail(InternalValidationError.invalidValue(error)) } - const error = InternalHelpDoc.p(`Expected to find option: '${this.fullName}'`) + const error = InternalHelpDoc.p(`Expected to find option: '${this.fullName()}'`) return Effect.fail(InternalValidationError.missingValue(error)) } @@ -241,12 +253,12 @@ export class Single implements Options.Options, Parameter.Input { return pipeArguments(this, arguments) } - private get placeholder(): string { - const pseudoName = Option.getOrElse(this.pseudoName, () => this.primitiveType.typeName) + private placeholder(): string { + const pseudoName = Option.getOrElse(this.pseudoName, () => this.primitiveType.typeName()) return `<${pseudoName}>` } - private get fullName(): string { + private fullName(): string { return this.makeFullName(this.name)[1] } @@ -265,30 +277,34 @@ export class Map implements Options.Options { readonly f: (a: A) => Either.Either ) {} - get identifier(): Option.Option { - return this.options.identifier - } - - get flattened(): ReadonlyArray { - return this.options.flattened + identifier(): Option.Option { + return this.options.identifier() } - get help(): HelpDoc.HelpDoc { - return this.options.help + flattened(): ReadonlyArray> { + return this.options.flattened() } - get usage(): Usage.Usage { - return this.options.usage + help(): HelpDoc.HelpDoc { + return this.options.help() } - get shortDescription(): string { - return this.options.shortDescription + usage(): Usage.Usage { + return this.options.usage() } modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { return new Map(this.options.modifySingle(f), this.f) } + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return this.options.wizard(config) + } + validate( args: HashMap.HashMap>, config: CliConfig.CliConfig @@ -311,34 +327,54 @@ export class OrElse implements Options.Options> { readonly right: Options.Options ) {} - get identifier(): Option.Option { - const ids = ReadonlyArray.compact([this.left.identifier, this.right.identifier]) + identifier(): Option.Option { + const ids = ReadonlyArray.compact([this.left.identifier(), this.right.identifier()]) return ReadonlyArray.match(ids, { onEmpty: () => Option.none(), onNonEmpty: (ids) => Option.some(ReadonlyArray.join(ids, ", ")) }) } - get flattened(): ReadonlyArray { - return ReadonlyArray.appendAll(this.left.flattened, this.right.flattened) + flattened(): ReadonlyArray> { + return ReadonlyArray.appendAll(this.left.flattened(), this.right.flattened()) } - get help(): HelpDoc.HelpDoc { - return InternalHelpDoc.sequence(this.left.help, this.right.help) + help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help(), this.right.help()) } - get usage(): Usage.Usage { - return InternalUsage.alternation(this.left.usage, this.right.usage) - } - - get shortDescription(): string { - return "" + usage(): Usage.Usage { + return InternalUsage.alternation(this.left.usage(), this.right.usage()) } modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options> { return new OrElse(this.left.modifySingle(f), this.right.modifySingle(f)) } + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const alternativeHelp = InternalHelpDoc.p("Select which option you would like to use") + const message = pipe( + wizardHeader, + InternalHelpDoc.sequence(this.help()), + InternalHelpDoc.sequence(alternativeHelp) + ) + const makeChoice = (title: string, value: Options.Options) => ({ title, value }) + const choices = ReadonlyArray.compact([ + Option.map(this.left.identifier(), (title) => makeChoice(title, this.left)), + Option.map(this.right.identifier(), (title) => makeChoice(title, this.right)) + ]) + return Console.log().pipe(Effect.zipRight( + InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices + }).pipe(Effect.flatMap((option) => option.wizard(config))) + )) + } + validate( args: HashMap.HashMap>, config: CliConfig.CliConfig @@ -368,8 +404,8 @@ export class OrElse implements Options.Options> { // The `identifier` will only be `None` for `Options.Empty`, which // means the user would have had to purposefully compose // `Options.Empty | otherArgument` - const leftUid = Option.getOrElse(this.left.identifier, () => "???") - const rightUid = Option.getOrElse(this.right.identifier, () => "???") + const leftUid = Option.getOrElse(this.left.identifier(), () => "???") + const rightUid = Option.getOrElse(this.right.identifier(), () => "???") const error = InternalHelpDoc.p( "Collision between two options detected - you can only specify " + `one of either: ['${leftUid}', '${rightUid}']` @@ -396,34 +432,42 @@ export class Both implements Options.Options { readonly right: Options.Options ) {} - get identifier(): Option.Option { - const ids = ReadonlyArray.compact([this.left.identifier, this.right.identifier]) + identifier(): Option.Option { + const ids = ReadonlyArray.compact([this.left.identifier(), this.right.identifier()]) return ReadonlyArray.match(ids, { onEmpty: () => Option.none(), onNonEmpty: (ids) => Option.some(ReadonlyArray.join(ids, ", ")) }) } - get flattened(): ReadonlyArray { - return ReadonlyArray.appendAll(this.left.flattened, this.right.flattened) + flattened(): ReadonlyArray> { + return ReadonlyArray.appendAll(this.left.flattened(), this.right.flattened()) } - get help(): HelpDoc.HelpDoc { - return InternalHelpDoc.sequence(this.left.help, this.right.help) + help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help(), this.right.help()) } - get usage(): Usage.Usage { - return InternalUsage.concat(this.left.usage, this.right.usage) - } - - get shortDescription(): string { - return "" + usage(): Usage.Usage { + return InternalUsage.concat(this.left.usage(), this.right.usage()) } modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { return new Both(this.left.modifySingle(f), this.right.modifySingle(f)) } + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + return Effect.zipWith( + this.left.wizard(config), + this.right.wizard(config), + (left, right) => ReadonlyArray.appendAll(left, right) + ) + } + validate( args: HashMap.HashMap>, config: CliConfig.CliConfig @@ -448,7 +492,7 @@ export class Both implements Options.Options { } /** @internal */ -export class WithDefault implements Options.Options, Parameter.Input { +export class WithDefault implements Options.Options.ParseableOptions { readonly [OptionsTypeId] = proto readonly _tag = "WithDefault" @@ -457,16 +501,16 @@ export class WithDefault implements Options.Options, Parameter.Input { readonly fallback: A ) {} - get identifier(): Option.Option { - return this.options.identifier + identifier(): Option.Option { + return this.options.identifier() } - get flattened(): ReadonlyArray { - return this.options.flattened + flattened(): ReadonlyArray> { + return this.options.flattened() } - get help(): HelpDoc.HelpDoc { - return InternalHelpDoc.mapDescriptionList(this.options.help, (span, block) => { + help(): HelpDoc.HelpDoc { + return InternalHelpDoc.mapDescriptionList(this.options.help(), (span, block) => { const optionalDescription = Option.isOption(this.fallback) ? Option.match(this.fallback, { onNone: () => InternalHelpDoc.p("This setting is optional."), @@ -477,36 +521,44 @@ export class WithDefault implements Options.Options, Parameter.Input { }) } - get usage(): Usage.Usage { - return InternalUsage.optional(this.options.usage) - } - - get shortDescription(): string { - return this.options.shortDescription + usage(): Usage.Usage { + return InternalUsage.optional(this.options.usage()) } modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { return new WithDefault(this.options.modifySingle(f), this.fallback) } - isValid( - input: string, - _config: CliConfig.CliConfig - ): Effect.Effect> { - return Effect.sync(() => { - if (isBool(this.options)) { - if (Schema.is(InternalPrimitive.trueValues)(input)) { - const identifier = Option.getOrElse(this.options.identifier, () => "") - return ReadonlyArray.of(identifier) - } - return ReadonlyArray.empty() - } - if (input.length === 0) { - return ReadonlyArray.empty() - } - const identifier = Option.getOrElse(this.options.identifier, () => "") - return ReadonlyArray.make(identifier, input) - }) + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + if (isBool(this.options)) { + return this.options.wizard(config) + } + const defaultHelp = InternalHelpDoc.p(`This option is optional - use the default?`) + const message = pipe( + wizardHeader, + InternalHelpDoc.sequence(this.options.help()), + InternalHelpDoc.sequence(defaultHelp) + ) + return Console.log().pipe( + Effect.zipRight( + InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Default ['${this.fallback}']`, value: true }, + { title: "Custom", value: false } + ] + }) + ), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(ReadonlyArray.empty()) + : this.options.wizard(config) + ) + ) } parse( @@ -537,31 +589,43 @@ export class WithDefault implements Options.Options, Parameter.Input { /** @internal */ export class KeyValueMap - implements Options.Options>, Parameter.Input + implements Options.Options.ParseableOptions> { readonly [OptionsTypeId] = proto readonly _tag = "KeyValueMap" constructor(readonly argumentOption: Single) {} - get identifier(): Option.Option { - return this.argumentOption.identifier + identifier(): Option.Option { + return this.argumentOption.identifier() } - get flattened(): ReadonlyArray { + flattened(): ReadonlyArray> { return ReadonlyArray.of(this) } - get help(): HelpDoc.HelpDoc { - return this.argumentOption.help - } - - get usage(): Usage.Usage { - return this.argumentOption.usage + help(): HelpDoc.HelpDoc { + // Single options always have an identifier, so we can safely `getOrThrow` + const identifier = Option.getOrThrow(this.argumentOption.identifier()) + return InternalHelpDoc.mapDescriptionList(this.argumentOption.help(), (span, oldBlock) => { + const header = InternalHelpDoc.p("This setting is a property argument which:") + const single = `${identifier} key1=value key2=value2'` + const multiple = `${identifier} key1=value ${identifier} key2=value2'` + const description = InternalHelpDoc.enumeration([ + InternalHelpDoc.p(`May be specified a single time: '${single}'`), + InternalHelpDoc.p(`May be specified multiple times: '${multiple}'`) + ]) + const newBlock = pipe( + oldBlock, + InternalHelpDoc.sequence(header), + InternalHelpDoc.sequence(description) + ) + return [span, newBlock] as const + }) } - get shortDescription(): string { - return this.argumentOption.shortDescription + usage(): Usage.Usage { + return this.argumentOption.usage() } modifySingle( @@ -570,14 +634,24 @@ export class KeyValueMap return new KeyValueMap(f(this.argumentOption)) } - isValid( - input: string, - config: CliConfig.CliConfig - ): Effect.Effect> { - const identifier = Option.getOrElse(this.identifier, () => "") - const args = input.split(" ") - return this.validate(HashMap.make([identifier, args]), config).pipe( - Effect.as(ReadonlyArray.prepend(args, identifier)) + wizard(config: CliConfig.CliConfig): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > { + const optionHelp = InternalHelpDoc.p("Enter `key=value` pairs separated by spaces") + const message = InternalHelpDoc.sequence(wizardHeader, optionHelp) + return Console.log().pipe( + Effect.zipRight(InternalListPrompt.list({ + message: InternalHelpDoc.toAnsiText(message).trim(), + delimiter: " " + })), + Effect.flatMap((args) => { + const identifier = Option.getOrElse(this.identifier(), () => "") + return this.validate(HashMap.make([identifier, args]), config).pipe( + Effect.as(ReadonlyArray.prepend(args, identifier)) + ) + }) ) } @@ -590,7 +664,7 @@ export class KeyValueMap readonly [ReadonlyArray, ReadonlyArray] > { const names = ReadonlyArray.map( - this.argumentOption.names, + this.argumentOption.names(), (name) => InternalCliConfig.normalizeCase(config, name) ) if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { @@ -947,7 +1021,7 @@ export const toRegularLanguage = ( } if (isSingle(self)) { const names = ReadonlyArray.reduce( - self.names, + self.names(), InternalRegularLanguage.empty, (lang, name) => InternalRegularLanguage.orElse(lang, InternalRegularLanguage.string(name)) ) @@ -1027,7 +1101,7 @@ export const validate = dual< readonly [Option.Option, ReadonlyArray, A] > >(3, (self, args, config) => - matchOptions(args, self.flattened, config).pipe( + matchOptions(args, self.flattened(), config).pipe( Effect.flatMap(([error, commandArgs, matchedOptions]) => self.validate(matchedOptions, config).pipe( Effect.catchAll((e) => @@ -1149,7 +1223,7 @@ const processArgs = ( */ const matchOptions = ( input: ReadonlyArray, - options: ReadonlyArray, + options: ReadonlyArray>, config: CliConfig.CliConfig ): Effect.Effect< never, @@ -1188,14 +1262,14 @@ const matchOptions = ( */ const findOptions = ( input: ReadonlyArray, - options: ReadonlyArray, + options: ReadonlyArray>, config: CliConfig.CliConfig ): Effect.Effect< never, ValidationError.ValidationError, readonly [ ReadonlyArray, - ReadonlyArray, + ReadonlyArray>, HashMap.HashMap> ] > => { @@ -1245,14 +1319,14 @@ const findOptions = ( const matchUnclustered = ( input: ReadonlyArray, tail: ReadonlyArray, - options: ReadonlyArray, + options: ReadonlyArray>, config: CliConfig.CliConfig ): Effect.Effect< never, ValidationError.ValidationError, readonly [ ReadonlyArray, - ReadonlyArray, + ReadonlyArray>, HashMap.HashMap> ] > => { diff --git a/src/internal/primitive.ts b/src/internal/primitive.ts index 493f762..458bcbf 100644 --- a/src/internal/primitive.ts +++ b/src/internal/primitive.ts @@ -1,4 +1,8 @@ import * as FileSystem from "@effect/platform/FileSystem" +// import * as AnsiRender from "@effect/printer-ansi/AnsiRender" +// import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" +// import * as Color from "@effect/printer-ansi/Color" +// import * as Doc from "@effect/printer/Doc" import * as Schema from "@effect/schema/Schema" import * as Effect from "effect/Effect" import { pipe } from "effect/Function" @@ -6,10 +10,19 @@ import * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" import * as ReadonlyArray from "effect/ReadonlyArray" import type * as CliConfig from "../CliConfig.js" +import type * as HelpDoc from "../HelpDoc.js" import type * as Span from "../HelpDoc/Span.js" import type * as Primitive from "../Primitive.js" +import type * as Prompt from "../Prompt.js" import * as InternalCliConfig from "./cliConfig.js" +import * as InternalHelpDoc from "./helpDoc.js" import * as InternalSpan from "./helpDoc/span.js" +import * as InternalPrompt from "./prompt.js" +import * as InternalDatePrompt from "./prompt/date.js" +import * as InternalNumberPrompt from "./prompt/number.js" +import * as InternalSelectPrompt from "./prompt/select.js" +import * as InternalTextPrompt from "./prompt/text.js" +import * as InternalTogglePrompt from "./prompt/toggle.js" const PrimitiveSymbolKey = "@effect/cli/Primitive" @@ -42,18 +55,29 @@ export class Bool implements Primitive.Primitive { constructor(readonly defaultValue: Option.Option) {} - get typeName(): string { + typeName(): string { return "boolean" } - get help(): Span.Span { + help(): Span.Span { return InternalSpan.text("A true or false value.") } - get choices(): Option.Option { + choices(): Option.Option { return Option.some("true | false") } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Select true or false") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalTogglePrompt.toggle({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + initial: Option.getOrElse(this.defaultValue, () => false), + active: "true", + inactive: "false" + }).pipe(InternalPrompt.map((bool) => `${bool}`)) + } + validate( value: Option.Option, config: CliConfig.CliConfig @@ -81,15 +105,13 @@ export class Choice implements Primitive.Primitive { readonly [PrimitiveTypeId] = proto readonly _tag = "Choice" - constructor( - readonly alternatives: ReadonlyArray.NonEmptyReadonlyArray - ) {} + constructor(readonly alternatives: ReadonlyArray.NonEmptyReadonlyArray) {} - get typeName(): string { + typeName(): string { return "choice" } - get help(): Span.Span { + help(): Span.Span { const choices = pipe( ReadonlyArray.map(this.alternatives, ([choice]) => choice), ReadonlyArray.join(", ") @@ -97,7 +119,7 @@ export class Choice implements Primitive.Primitive { return InternalSpan.text(`One of the following: ${choices}`) } - get choices(): Option.Option { + choices(): Option.Option { const choices = pipe( ReadonlyArray.map(this.alternatives, ([choice]) => choice), ReadonlyArray.join(" | ") @@ -105,6 +127,15 @@ export class Choice implements Primitive.Primitive { return Option.some(choices) } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Select one of the following choices") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: this.alternatives.map(([title]) => ({ title, value: title })) + }) + } + validate( value: Option.Option, _config: CliConfig.CliConfig @@ -114,10 +145,7 @@ export class Choice implements Primitive.Primitive { () => `Choice options to not have a default value` ).pipe( Effect.flatMap((value) => - ReadonlyArray.findFirst( - this.alternatives, - ([choice]) => choice === value - ) + ReadonlyArray.findFirst(this.alternatives, ([choice]) => choice === value) ), Effect.mapBoth({ onFailure: () => { @@ -146,27 +174,35 @@ export class Date implements Primitive.Primitive { readonly [PrimitiveTypeId] = proto readonly _tag = "Date" - get typeName(): string { + typeName(): string { return "date" } - get help(): Span.Span { + help(): Span.Span { return InternalSpan.text( "A date without a time-zone in the ISO-8601 format, such as 2007-12-03T10:15:30." ) } - get choices(): Option.Option { + choices(): Option.Option { return Option.some("date") } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Enter a date") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalDatePrompt.date({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }).pipe(InternalPrompt.map((date) => date.toISOString())) + } + validate( value: Option.Option, _config: CliConfig.CliConfig ): Effect.Effect { return attempt( value, - this.typeName, + this.typeName(), Schema.parse(Schema.dateFromString(Schema.string)) ) } @@ -185,24 +221,32 @@ export class Float implements Primitive.Primitive { readonly [PrimitiveTypeId] = proto readonly _tag = "Float" - get typeName(): string { + typeName(): string { return "float" } - get help(): Span.Span { + help(): Span.Span { return InternalSpan.text("A floating point number.") } - get choices(): Option.Option { + choices(): Option.Option { return Option.none() } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Enter a floating point value") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalNumberPrompt.float({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }).pipe(InternalPrompt.map((value) => `${value}`)) + } + validate( value: Option.Option, _config: CliConfig.CliConfig ): Effect.Effect { const numberFromString = Schema.string.pipe(Schema.numberFromString) - return attempt(value, this.typeName, Schema.parse(numberFromString)) + return attempt(value, this.typeName(), Schema.parse(numberFromString)) } pipe() { @@ -219,24 +263,32 @@ export class Integer implements Primitive.Primitive { readonly [PrimitiveTypeId] = proto readonly _tag = "Integer" - get typeName(): string { + typeName(): string { return "integer" } - get help(): Span.Span { + help(): Span.Span { return InternalSpan.text("An integer.") } - get choices(): Option.Option { + choices(): Option.Option { return Option.none() } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Enter an integer") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalNumberPrompt.float({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }).pipe(InternalPrompt.map((value) => `${value}`)) + } + validate( value: Option.Option, _config: CliConfig.CliConfig ): Effect.Effect { const intFromString = Schema.string.pipe(Schema.numberFromString, Schema.int()) - return attempt(value, this.typeName, Schema.parse(intFromString)) + return attempt(value, this.typeName(), Schema.parse(intFromString)) } pipe() { @@ -254,14 +306,14 @@ export class Path implements Primitive.Primitive { readonly pathExists: Primitive.Primitive.PathExists ) {} - get typeName(): string { + typeName(): string { if (this.pathType === "either") { return "path" } return this.pathType } - get help(): Span.Span { + help(): Span.Span { if (this.pathType === "either" && this.pathExists === "yes") { return InternalSpan.text("An existing file or directory.") } @@ -295,10 +347,18 @@ export class Path implements Primitive.Primitive { ) } - get choices(): Option.Option { + choices(): Option.Option { return Option.none() } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Enter a file system path") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalTextPrompt.text({ + message: InternalHelpDoc.toAnsiText(message).trimEnd() + }) + } + validate( value: Option.Option, _config: CliConfig.CliConfig @@ -337,23 +397,29 @@ export class Text implements Primitive.Primitive { readonly [PrimitiveTypeId] = proto readonly _tag = "Text" - get typeName(): string { + typeName(): string { return "text" } - get help(): Span.Span { + help(): Span.Span { return InternalSpan.text("A user-defined piece of text.") } - get choices(): Option.Option { + choices(): Option.Option { return Option.none() } + wizard(help: HelpDoc.HelpDoc): Prompt.Prompt { + const primitiveHelp = InternalHelpDoc.p("Enter some text") + const message = InternalHelpDoc.sequence(help, primitiveHelp) + return InternalTextPrompt.text({ message: InternalHelpDoc.toAnsiText(message).trimEnd() }) + } + validate( value: Option.Option, _config: CliConfig.CliConfig ): Effect.Effect { - return attempt(value, this.typeName, Schema.parse(Schema.string)) + return attempt(value, this.typeName(), Schema.parse(Schema.string)) } pipe() { diff --git a/src/internal/prompt.ts b/src/internal/prompt.ts index 14ff106..7c862b1 100644 --- a/src/internal/prompt.ts +++ b/src/internal/prompt.ts @@ -1,9 +1,14 @@ +import * as Terminal from "@effect/platform/Terminal" import * as AnsiRender from "@effect/printer-ansi/AnsiRender" -import { Effect, Effectable, Function, pipe, Pipeable, Ref } from "effect" +import * as Effect from "effect/Effect" +import * as Effectable from "effect/Effectable" +import { dual } from "effect/Function" +import * as Option from "effect/Option" +import * as Pipeable from "effect/Pipeable" +import * as Ref from "effect/Ref" import type * as Prompt from "../Prompt.js" -import type * as Terminal from "../Terminal.js" -import * as ansiUtils from "./prompt/ansi-utils.js" -import * as terminal from "./terminal.js" +import type * as PromptAction from "../Prompt/Action.js" +import * as InternalAnsiUtils from "./prompt/ansi-utils.js" /** @internal */ const PromptSymbolKey = "@effect/cli/Prompt" @@ -40,11 +45,12 @@ export interface Loop extends Op<"Loop", { readonly initialState: unknown readonly render: ( - state: unknown, + prevState: Option.Option, + nextState: unknown, action: Prompt.Prompt.Action ) => Effect.Effect readonly process: ( - input: Terminal.Terminal.UserInput, + input: Terminal.UserInput, state: unknown ) => Effect.Effect> }> @@ -106,11 +112,12 @@ export const all: < export const custom = ( initialState: State, render: ( - state: State, + prevState: Option.Option, + nextState: State, action: Prompt.Prompt.Action ) => Effect.Effect, process: ( - input: Terminal.Terminal.UserInput, + input: Terminal.UserInput, state: State ) => Effect.Effect> ): Prompt.Prompt => { @@ -123,7 +130,7 @@ export const custom = ( } /** @internal */ -export const map = Function.dual< +export const map = dual< ( f: (output: Output) => Output2 ) => ( @@ -136,7 +143,7 @@ export const map = Function.dual< >(2, (self, f) => flatMap(self, (a) => succeed(f(a)))) /** @internal */ -export const flatMap = Function.dual< +export const flatMap = dual< ( f: (output: Output) => Prompt.Prompt ) => ( @@ -158,44 +165,53 @@ export const flatMap = Function.dual< export const run = ( self: Prompt.Prompt ): Effect.Effect => - Effect.flatMap(terminal.Tag, (terminal) => { + Effect.flatMap(Terminal.Terminal, (terminal) => { const op = self as Primitive switch (op._tag) { case "Loop": { - return pipe( - Ref.make(op.initialState), - Effect.flatMap((ref) => { + return Effect.all([ + Ref.make(Option.none()), + Ref.make(op.initialState) + ]).pipe( + Effect.flatMap(([prevStateRef, nextStateRef]) => { const loop = ( - action: Exclude, { _tag: "Submit" }> + action: Exclude, { _tag: "Submit" }> ): Effect.Effect => - Effect.flatMap(Ref.get(ref), (state) => - pipe( - op.render(state, action), - Effect.flatMap(terminal.display), - Effect.zipRight(terminal.getUserInput), - Effect.flatMap((input) => op.process(input, state)), - Effect.flatMap((action) => { - switch (action._tag) { - case "NextFrame": { - return Effect.zipRight(Ref.set(ref, action.state), loop(action)) + Effect.all([Ref.get(prevStateRef), Ref.get(nextStateRef)]).pipe( + Effect.flatMap(([prevState, nextState]) => + op.render(prevState, nextState, action).pipe( + Effect.flatMap((msg) => Effect.orDie(terminal.display(msg))), + Effect.zipRight(terminal.readInput), + Effect.catchTag("QuitException", (e) => Effect.die(e)), + Effect.flatMap((input) => op.process(input, nextState)), + Effect.flatMap((action) => { + switch (action._tag) { + case "Beep": { + return loop(action) + } + case "NextFrame": { + return Ref.set(prevStateRef, Option.some(nextState)).pipe( + Effect.zipRight(Ref.set(nextStateRef, action.state)), + Effect.zipRight(loop(action)) + ) + } + case "Submit": { + return op.render(prevState, nextState, action).pipe( + Effect.flatMap((msg) => Effect.orDie(terminal.display(msg))), + Effect.zipRight(Effect.succeed(action.value)) + ) + } } - case "Submit": { - return pipe( - op.render(state, action), - Effect.flatMap(terminal.display), - Effect.zipRight(Effect.succeed(action.value)) - ) - } - default: { - return loop(action) - } - } - }) - )) + }) + ) + ) + ) return loop({ _tag: "NextFrame", state: op.initialState }) }), // Always make sure to restore the display of the cursor - Effect.ensuring(terminal.display(AnsiRender.prettyDefault(ansiUtils.cursorShow))) + Effect.ensuring(Effect.orDie( + terminal.display(AnsiRender.prettyDefault(InternalAnsiUtils.cursorShow)) + )) ) } case "OnSuccess": { diff --git a/src/internal/prompt/action.ts b/src/internal/prompt/action.ts index 5796bea..45d1c84 100644 --- a/src/internal/prompt/action.ts +++ b/src/internal/prompt/action.ts @@ -5,12 +5,6 @@ export const beep: PromptAction.PromptAction = { _tag: "Beep" } -/** @internal */ -export const error = (message: string): PromptAction.PromptAction => ({ - _tag: "Error", - message -}) - /** @internal */ export const nextFrame = (state: State): PromptAction.PromptAction => ({ _tag: "NextFrame", diff --git a/src/internal/prompt/ansi-utils.ts b/src/internal/prompt/ansi-utils.ts index 7486d7b..3a5a88a 100644 --- a/src/internal/prompt/ansi-utils.ts +++ b/src/internal/prompt/ansi-utils.ts @@ -1,6 +1,8 @@ import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" import * as Doc from "@effect/printer/Doc" -import { Effect } from "effect" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as ReadonlyArray from "effect/ReadonlyArray" const defaultFigures = { arrowUp: Doc.text("↑"), @@ -50,188 +52,219 @@ const CSI = `${ESC}[` export const beep: AnsiDoc.AnsiDoc = Doc.char(BEEP) /** - * Clear from cursor to beginning of the screen. + * Moves the cursor to the specified `row` and `column`. * - * @internal - */ -export const clearUp: AnsiDoc.AnsiDoc = Doc.text(`${CSI}1J`) - -/** - * Clear from cursor to end of screen. + * Though the ANSI Control Sequence for Cursor Position is `1`-based, this + * method takes row and column values starting from `0` and adjusts them to `1`- + * based values. * * @internal */ -export const clearDown: AnsiDoc.AnsiDoc = Doc.text(`${CSI}J`) +export const cursorTo = (row: number, column: number = 0): AnsiDoc.AnsiDoc => { + if (column === 0) { + return Doc.text(`${CSI}${row + 1}G`) + } + return Doc.text(`${CSI}${column + 1};${row + 1}H`) +} /** - * Clear from cursor to the start of the line. Cursor position does not change. + * Move the cursor position the specified number of `rows` and `columns` + * relative to the current cursor position. + * + * If the cursor is already at the edge of the screen in either direction, then + * additional movement will have no effect. * * @internal */ -export const clearLeft: AnsiDoc.AnsiDoc = Doc.text(`${CSI}$1K`) +export const cursorMove = (rows: number, columns: number = 0): AnsiDoc.AnsiDoc => { + let move: AnsiDoc.AnsiDoc = Doc.empty + if (rows > 0) { + move = Doc.cat(move, cursorForward(rows)) + } else if (rows < 0) { + move = Doc.cat(move, cursorBackward(-rows)) + } + if (columns > 0) { + move = Doc.cat(move, cursorDown(columns)) + } else if (columns < 0) { + move = Doc.cat(move, cursorUp(-columns)) + } + return move +} /** - * Clear from cursor to the end of the line. Cursor position does not change. + * Moves the cursor up by the specified number of `rows` (default `1`) relative + * to the current cursor position. + * + * If the cursor is already at the edge of the screen, this has no effect. * * @internal */ -export const clearRight: AnsiDoc.AnsiDoc = Doc.text(`${CSI}K`) +export const cursorUp = (rows: number = 1): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${rows}A`) /** - * Clear entire screen. And moves cursor to upper left on DOS. + * Moves the cursor down by the specified number of `rows` (default `1`) + * relative to the current cursor position. + * + * If the cursor is already at the edge of the screen, this has no effect. * * @internal */ -export const clearScreen: AnsiDoc.AnsiDoc = Doc.text(`${CSI}2J`) +export const cursorDown = (rows: number = 1): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${rows}B`) /** - * Clear the current line. Cursor position does not change. + * Moves the cursor backward by the specified number of `columns` (default `1`) + * relative to the current cursor position. + * + * If the cursor is already at the edge of the screen, this has no effect. * * @internal */ -export const clearLine: AnsiDoc.AnsiDoc = Doc.text(`${CSI}2K`) +export const cursorBackward = (columns: number = 1): AnsiDoc.AnsiDoc => + Doc.text(`${CSI}${columns}D`) /** - * Sets the cursor position to the absolute coordinates `x` and `y`. + * Moves the cursor forward by the specified number of `columns` (default `1`) + * relative to the current cursor position. + * + * If the cursor is already at the edge of the screen, this has no effect. * * @internal */ -export const setCursorPosition = (x: number, y?: number): AnsiDoc.AnsiDoc => { - if (y === undefined) { - return Doc.text(`${CSI}${x + 1}G`) - } - return Doc.text(`${CSI}${y + 1};${x + 1}H`) -} +export const cursorForward = (columns: number = 1): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${columns}C`) /** - * Clears the current line and resets the cursor position to the beginning of - * the line. + * Moves the cursor to the first column. * * @internal */ -export const resetLine: AnsiDoc.AnsiDoc = Doc.cat(clearLine, setCursorPosition(0)) - -const strip = (str: string) => { - const pattern = [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" - ].join("|") - const regex = new RegExp(pattern, "g") - return str.replace(regex, "") -} - -const width = (str: string) => [...strip(str)].length +export const cursorLeft: AnsiDoc.AnsiDoc = Doc.text(`${CSI}G`) /** + * Saves the cursor position, encoding shift state and formatting attributes. + * * @internal */ -export const resetLines = (prompt: string, perLine?: number) => { - if (!perLine) { - return resetLine - } - let rows = 0 - const lines = prompt.split(/\r?\n/) - for (const line of lines) { - rows += 1 + Math.floor(Math.max(width(line) - 1, 0) / perLine) - } - return clearLines(rows) -} +export const cursorSave: AnsiDoc.AnsiDoc = Doc.text(`${ESC}7`) /** - * Clears from the cursor to the end of the screen and resets the cursor - * position to the beginning of the line. + * Restores the cursor position, encoding shift state and formatting attributes + * from the previous save, if any, otherwise resets these all to their defaults. * * @internal */ -export const resetDown: AnsiDoc.AnsiDoc = Doc.cat(clearDown, setCursorPosition(0)) +export const cursorRestore: AnsiDoc.AnsiDoc = Doc.text(`${ESC}8`) /** - * Clear the specified number of lines. + * Saves the current cursor position. * * @internal */ -export const clearLines = (lines: number): AnsiDoc.AnsiDoc => { - let clear: AnsiDoc.AnsiDoc = Doc.empty - for (let i = 0; i < lines; i++) { - clear = Doc.cat(clear, Doc.cat(clearLine, i < lines - 1 ? moveCursorUp(1) : Doc.empty)) - } - if (lines > 0) { - clear = Doc.cat(clear, moveCursorLeft(1)) - } - return clear -} +export const cursorSavePosition: AnsiDoc.AnsiDoc = Doc.text(`${ESC}s`) /** - * Hides the cursor. + * Restores the cursor position from the previous save. * * @internal */ -export const cursorHide: AnsiDoc.AnsiDoc = Doc.text(`${CSI}?25l`) +export const cursorRestorePosition: AnsiDoc.AnsiDoc = Doc.text(`${ESC}u`) /** - * Shows the cursor. + * Reports the cursor position (CPR) by transmitting `ESC[n;mR`, where `n` is + * the row and `m` is the column. * * @internal */ -export const cursorShow: AnsiDoc.AnsiDoc = Doc.text(`${CSI}?25h`) +export const cursorGetPosition: AnsiDoc.AnsiDoc = Doc.text(`${ESC}6n`) /** - * Saves the position of the cursor. + * Moves cursor to beginning of the line the specified number of rows down + * (default `1`). * * @internal */ -export const cursorSave: AnsiDoc.AnsiDoc = Doc.text(`${ESC}7`) +export const cursorNextLine = (rows: number = 1): AnsiDoc.AnsiDoc => Doc.text(`${ESC}${rows}E`) /** - * Saves the position of the cursor. - * + * Moves cursor to beginning of the line the specified number of rows up + * (default `1`). * @internal */ -export const cursorRestore: AnsiDoc.AnsiDoc = Doc.text(`${ESC}8`) +export const cursorPreviousLine = (rows: number = 1): AnsiDoc.AnsiDoc => Doc.text(`${ESC}${rows}F`) /** - * Move the cursor up by the specified number of `lines`. + * Hides the cursor. * * @internal */ -export const moveCursorUp = (lines: number): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${lines}A`) +export const cursorHide: AnsiDoc.AnsiDoc = Doc.text(`${CSI}?25l`) /** - * Move the cursor down by the specified number of `lines`. + * Shows the cursor. * * @internal */ -export const moveCursorDown = (lines: number): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${lines}B`) +export const cursorShow: AnsiDoc.AnsiDoc = Doc.text(`${CSI}?25h`) /** - * Move the cursor left by the specified number of `columns`. + * Erases the entire current line. The cursor position does not change. * * @internal */ -export const moveCursorLeft = (columns: number): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${columns}D`) +export const eraseLine: AnsiDoc.AnsiDoc = Doc.text(`${CSI}2K`) + /** - * Move the cursor right by the specified number of `columns`. + * Erase from the current cursor position up the specified amount of rows. * * @internal */ -export const moveCursorRight = (columns: number): AnsiDoc.AnsiDoc => Doc.text(`${CSI}${columns}C`) +export const eraseLines = (rows: number): AnsiDoc.AnsiDoc => { + let clear: AnsiDoc.AnsiDoc = Doc.empty + for (let i = 0; i < rows; i++) { + clear = Doc.cat(clear, Doc.cat(eraseLine, i < rows - 1 ? cursorUp(1) : Doc.empty)) + } + if (rows > 0) { + clear = Doc.cat(clear, cursorLeft) + } + return clear +} /** - * Move the cursor position by the relative coordinates `x` and `y`. + * Clears all lines taken up by the specified `text`. * * @internal */ -export const moveCursor = (x: number, y = 0): AnsiDoc.AnsiDoc => { - let move: AnsiDoc.AnsiDoc = Doc.empty - if (x > 0) { - move = Doc.cat(move, moveCursorRight(x)) - } else if (x < 0) { - move = Doc.cat(move, moveCursorLeft(-x)) +export const eraseText = (text: string, columns: number): AnsiDoc.AnsiDoc => { + if (columns === 0) { + return Doc.cat(eraseLine, cursorTo(0)) } - if (y > 0) { - move = Doc.cat(move, moveCursorDown(y)) - } else if (y < 0) { - move = Doc.cat(move, moveCursorUp(-y)) + let rows = 0 + const lines = text.split(/\r?\n/) + for (const line of lines) { + rows += 1 + Math.floor(Math.max(width(line) - 1, 0) / columns) } - return move + return eraseLines(rows) +} + +/** @internal */ +export const strip = (str: string) => { + const pattern = [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + ].join("|") + const regex = new RegExp(pattern, "g") + return str.replace(regex, "") +} + +/** @internal */ +export const width = (str: string) => [...strip(str)].length + +/** @internal */ +export const lines = (prompt: string, columns: number): number => { + const lines = strip(prompt).split(/\r?\n/) + return columns === 0 + ? lines.length + : pipe( + ReadonlyArray.map(lines, (line) => Math.ceil(line.length / columns)), + ReadonlyArray.reduce(0, (left, right) => left + right) + ) } diff --git a/src/internal/prompt/confirm.ts b/src/internal/prompt/confirm.ts new file mode 100644 index 0000000..5b01733 --- /dev/null +++ b/src/internal/prompt/confirm.ts @@ -0,0 +1,164 @@ +import * as Terminal from "@effect/platform/Terminal" +import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" +import * as AnsiRender from "@effect/printer-ansi/AnsiRender" +import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" +import * as Color from "@effect/printer-ansi/Color" +import * as Doc from "@effect/printer/Doc" +import * as Optimize from "@effect/printer/Optimize" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" +import type * as Prompt from "../../Prompt.js" +import type * as PromptAction from "../../Prompt/Action.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalPromptAction from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface State { + readonly value: boolean +} + +const renderBeep = AnsiRender.prettyDefault(InternalAnsiUtils.beep) + +const renderClearScreen = ( + prevState: Option.Option, + options: Required, + columns: number +): AnsiDoc.AnsiDoc => { + const clearPrompt = Doc.cat(InternalAnsiUtils.eraseLine, InternalAnsiUtils.cursorLeft) + if (Option.isNone(prevState)) { + return clearPrompt + } + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + return Doc.cat(clearOutput, clearPrompt) +} + +const renderOutput = ( + confirm: AnsiDoc.AnsiDoc, + leadingSymbol: AnsiDoc.AnsiDoc, + trailingSymbol: AnsiDoc.AnsiDoc, + options: Required +): AnsiDoc.AnsiDoc => { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate(Doc.text(line), AnsiStyle.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (ReadonlyArray.isNonEmptyReadonlyArray(promptLines)) { + const lines = ReadonlyArray.map(promptLines, (line) => annotateLine(line)) + return pipe( + prefix, + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(confirm) + ) + } + return Doc.hsep([prefix, trailingSymbol, confirm]) +} + +const renderNextFrame = ( + prevState: Option.Option, + nextState: State, + options: Required +): Effect.Effect => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(prevState, options, terminal.columns) + const leadingSymbol = Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan)) + const trailingSymbol = Doc.annotate(figures.pointerSmall, AnsiStyle.color(Color.black)) + const confirmAnnotation = AnsiStyle.color(Color.black) + // Marking these explicitly as present with `!` because they always will be + // and there is really no value in adding a `DeepRequired` type helper just + // for these internal cases + const confirmMessage = nextState.value + ? options.placeholder.defaultConfirm! + : options.placeholder.defaultDeny! + const confirm = Doc.annotate(Doc.text(confirmMessage), confirmAnnotation) + const promptMsg = renderOutput(confirm, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(InternalAnsiUtils.cursorHide), + Doc.cat(promptMsg), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const renderSubmission = ( + nextState: State, + value: boolean, + options: Required +) => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(Option.some(nextState), options, terminal.columns) + const leadingSymbol = Doc.annotate(figures.tick, AnsiStyle.color(Color.green)) + const trailingSymbol = Doc.annotate(figures.ellipsis, AnsiStyle.color(Color.black)) + const confirmMessage = value ? options.label.confirm : options.label.deny + const confirm = Doc.text(confirmMessage) + const promptMsg = renderOutput(confirm, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const TRUE_VALUE_REGEX = /^y|t$/ +const FALSE_VALUE_REGEX = /^n|f$/ + +const processInputValue = ( + value: string +): Effect.Effect> => { + if (TRUE_VALUE_REGEX.test(value.toLowerCase())) { + return Effect.succeed(InternalPromptAction.submit(true)) + } + if (FALSE_VALUE_REGEX.test(value.toLowerCase())) { + return Effect.succeed(InternalPromptAction.submit(false)) + } + return Effect.succeed(InternalPromptAction.beep) +} + +/** @internal */ +export const confirm = (options: Prompt.Prompt.ConfirmOptions): Prompt.Prompt => { + const opts: Required = { + initial: false, + ...options, + label: { + confirm: "yes", + deny: "no", + ...options.label + }, + placeholder: { + defaultConfirm: "(Y/n)", + defaultDeny: "(y/N)", + ...options.placeholder + } + } + return InternalPrompt.custom( + { value: opts.initial } as State, + (prevState, nextState, action) => { + switch (action._tag) { + case "Beep": { + return Effect.succeed(renderBeep) + } + case "NextFrame": { + return renderNextFrame(prevState, nextState, opts) + } + case "Submit": { + return renderSubmission(nextState, action.value, opts) + } + } + }, + (input, _) => { + const value = Option.getOrElse(input.input, () => "") + return processInputValue(value) + } + ) +} diff --git a/src/internal/prompt/date.ts b/src/internal/prompt/date.ts new file mode 100644 index 0000000..ba149e6 --- /dev/null +++ b/src/internal/prompt/date.ts @@ -0,0 +1,636 @@ +import * as Terminal from "@effect/platform/Terminal" +import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" +import * as AnsiRender from "@effect/printer-ansi/AnsiRender" +import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" +import * as Color from "@effect/printer-ansi/Color" +import * as Doc from "@effect/printer/Doc" +import * as Optimize from "@effect/printer/Optimize" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Match from "effect/Match" +import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalPromptAction from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface State { + readonly typed: string + readonly cursor: number + readonly value: globalThis.Date + readonly dateParts: ReadonlyArray + readonly error: Option.Option +} + +const renderBeep = AnsiRender.prettyDefault(InternalAnsiUtils.beep) + +const renderClearScreen = ( + prevState: Option.Option, + options: Required, + columns: number +): AnsiDoc.AnsiDoc => { + const clearPrompt = Doc.cat(InternalAnsiUtils.eraseLine, InternalAnsiUtils.cursorLeft) + if (Option.isNone(prevState)) { + return clearPrompt + } + const clearError = Option.match(prevState.value.error, { + onNone: () => Doc.empty, + onSome: (error) => + pipe( + InternalAnsiUtils.cursorDown(InternalAnsiUtils.lines(error, columns)), + Doc.cat(InternalAnsiUtils.eraseText(`\n${error}`, columns)) + ) + }) + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + return Doc.cat(clearError, Doc.cat(clearOutput, clearPrompt)) +} + +const renderError = (nextState: State, pointer: AnsiDoc.AnsiDoc): AnsiDoc.AnsiDoc => + Option.match(nextState.error, { + onNone: () => Doc.empty, + onSome: (error) => { + const errorLines = error.split(/\r?\n/) + if (ReadonlyArray.isNonEmptyReadonlyArray(errorLines)) { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate( + Doc.text(line), + AnsiStyle.combine(AnsiStyle.italicized, AnsiStyle.color(Color.red)) + ) + const prefix = Doc.cat(Doc.annotate(pointer, AnsiStyle.color(Color.red)), Doc.space) + const lines = ReadonlyArray.map(errorLines, (str) => annotateLine(str)) + return pipe( + InternalAnsiUtils.cursorSave, + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(InternalAnsiUtils.cursorRestore) + ) + } + return Doc.empty + } + }) + +const renderParts = (nextState: State, submitted: boolean = false) => + ReadonlyArray.reduce( + nextState.dateParts, + Doc.empty as AnsiDoc.AnsiDoc, + (doc, part, currentIndex) => { + const partDoc = Doc.text(part.toString()) + if (currentIndex === nextState.cursor && !submitted) { + const annotation = AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.cyan)) + return Doc.cat(doc, Doc.annotate(partDoc, annotation)) + } + return Doc.cat(doc, partDoc) + } + ) + +const renderOutput = ( + nextState: State, + leadingSymbol: AnsiDoc.AnsiDoc, + trailingSymbol: AnsiDoc.AnsiDoc, + parts: AnsiDoc.AnsiDoc, + options: Required +): AnsiDoc.AnsiDoc => { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate(Doc.text(line), AnsiStyle.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (ReadonlyArray.isNonEmptyReadonlyArray(promptLines)) { + const lines = ReadonlyArray.map(promptLines, (line) => annotateLine(line)) + return pipe( + prefix, + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(parts) + ) + } + return Doc.hsep([prefix, trailingSymbol, parts]) +} + +const renderNextFrame = ( + prevState: Option.Option, + nextState: State, + options: Required +): Effect.Effect => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(prevState, options, terminal.columns) + const leadingSymbol = Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan)) + const trailingSymbol = Doc.annotate(figures.pointerSmall, AnsiStyle.color(Color.black)) + const parts = renderParts(nextState) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, parts, options) + const errorMsg = renderError(nextState, figures.pointerSmall) + return pipe( + clearScreen, + Doc.cat(InternalAnsiUtils.cursorHide), + Doc.cat(promptMsg), + Doc.cat(errorMsg), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const renderSubmission = (nextState: State, options: Required) => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(Option.some(nextState), options, terminal.columns) + const leadingSymbol = Doc.annotate(figures.tick, AnsiStyle.color(Color.green)) + const trailingSymbol = Doc.annotate(figures.ellipsis, AnsiStyle.color(Color.black)) + const parts = renderParts(nextState, true) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, parts, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const processUp = (currentState: State) => { + currentState.dateParts[currentState.cursor].increment() + return InternalPromptAction.nextFrame({ + ...currentState, + typed: "" + }) +} + +const processDown = (currentState: State) => { + currentState.dateParts[currentState.cursor].decrement() + return InternalPromptAction.nextFrame({ + ...currentState, + typed: "" + }) +} + +const processCursorLeft = (currentState: State) => { + const previousPart = currentState.dateParts[currentState.cursor].previousPart() + return Option.match(previousPart, { + onNone: () => InternalPromptAction.beep, + onSome: (previous) => + InternalPromptAction.nextFrame({ + ...currentState, + typed: "", + cursor: currentState.dateParts.indexOf(previous) + }) + }) +} + +const processCursorRight = (currentState: State) => { + const nextPart = currentState.dateParts[currentState.cursor].nextPart() + return Option.match(nextPart, { + onNone: () => InternalPromptAction.beep, + onSome: (next) => + InternalPromptAction.nextFrame({ + ...currentState, + typed: "", + cursor: currentState.dateParts.indexOf(next) + }) + }) +} + +const processNext = (currentState: State) => { + const nextPart = currentState.dateParts[currentState.cursor].nextPart() + const cursor = Option.match(nextPart, { + onNone: () => currentState.dateParts.findIndex((part) => !part.isToken()), + onSome: (next) => currentState.dateParts.indexOf(next) + }) + return InternalPromptAction.nextFrame({ + ...currentState, + cursor + }) +} + +const defaultProcessor = (value: string, currentState: State) => { + if (/\d/.test(value)) { + const typed = currentState.typed + value + currentState.dateParts[currentState.cursor].setValue(typed) + return InternalPromptAction.nextFrame({ + ...currentState, + typed + }) + } + return InternalPromptAction.beep +} + +const defaultLocales: Prompt.Prompt.DateOptions["locales"] = { + months: [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + weekdays: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] +} + +/** @internal */ +export const date = (options: Prompt.Prompt.DateOptions): Prompt.Prompt => { + const opts: Required = { + initial: new Date(), + dateMask: "YYYY-MM-DD HH:mm:ss", + validate: Effect.succeed, + ...options, + locales: { + ...defaultLocales, + ...options.locales + } + } + const dateParts = makeDateParts(opts.dateMask, opts.initial, opts.locales) + const initialCursorPosition = dateParts.findIndex((part) => !part.isToken()) + return InternalPrompt.custom( + { + dateParts, + typed: "", + cursor: initialCursorPosition, + value: opts.initial, + error: Option.none() + } as State, + (prevState, nextState, action) => { + switch (action._tag) { + case "Beep": { + return Effect.succeed(renderBeep) + } + case "NextFrame": { + return renderNextFrame(prevState, nextState, opts) + } + case "Submit": { + return renderSubmission(nextState, opts) + } + } + }, + (input, state) => { + switch (input.key.name) { + case "left": { + return Effect.succeed(processCursorLeft(state)) + } + case "right": { + return Effect.succeed(processCursorRight(state)) + } + case "k": + case "up": { + return Effect.succeed(processUp(state)) + } + case "j": + case "down": { + return Effect.succeed(processDown(state)) + } + case "tab": { + return Effect.succeed(processNext(state)) + } + case "enter": + case "return": { + return Effect.match(opts.validate(state.value), { + onFailure: (error) => + InternalPromptAction.nextFrame({ + ...state, + error: Option.some(error) + }), + onSuccess: InternalPromptAction.submit + }) + } + default: { + const value = Option.getOrElse(input.input, () => "") + return Effect.succeed(defaultProcessor(value, state)) + } + } + } + ) +} + +const DATE_PART_REGEX = + /\\(.)|"((?:\\["\\]|[^"])+)"|(D[Do]?|d{3,4}|d)|(M{1,4})|(YY(?:YY)?)|([aA])|([Hh]{1,2})|(m{1,2})|(s{1,2})|(S{1,4})|./g + +const regexGroups: Record DatePart> = { + 1: ({ token, ...opts }) => new Token({ token: token.replace(/\\(.)/g, "$1"), ...opts }), + 2: (opts) => new Day(opts), + 3: (opts) => new Month(opts), + 4: (opts) => new Year(opts), + 5: (opts) => new Meridiem(opts), + 6: (opts) => new Hours(opts), + 7: (opts) => new Minutes(opts), + 8: (opts) => new Seconds(opts), + 9: (opts) => new Milliseconds(opts) +} + +const makeDateParts = ( + dateMask: string, + date: globalThis.Date, + locales: Prompt.Prompt.DateOptions["locales"] +) => { + const parts: Array = [] + let result: RegExpExecArray | null = null + // eslint-disable-next-line no-cond-assign + while (result = DATE_PART_REGEX.exec(dateMask)) { + const match = result.shift() + const index = result.findIndex((group) => group !== undefined) + if (index in regexGroups) { + const token = (result[index] || match)! + parts.push(regexGroups[index]({ token, date, parts, locales })) + } else { + parts.push(new Token({ token: (result[index] || match)!, date, parts, locales })) + } + } + const orderedParts = parts.reduce((array, element) => { + const lastElement = array[array.length - 1] + if (element.isToken() && lastElement !== undefined && lastElement.isToken()) { + lastElement.setValue(element.token) + } else { + array.push(element) + } + return array + }, ReadonlyArray.empty()) + parts.splice(0, parts.length, ...orderedParts) + return parts +} + +interface DatePartParams { + readonly token: string + readonly locales: Prompt.Prompt.DateOptions["locales"] + readonly date?: globalThis.Date + readonly parts?: ReadonlyArray +} + +abstract class DatePart { + token: string + readonly date: globalThis.Date + readonly parts: ReadonlyArray + readonly locales: Prompt.Prompt.DateOptions["locales"] + + constructor(params: DatePartParams) { + this.token = params.token + this.locales = params.locales + this.date = params.date || new Date() + this.parts = params.parts || [this] + } + + /** + * Increments this date part. + */ + abstract increment(): void + + /** + * Decrements this date part. + */ + abstract decrement(): void + + /** + * Sets the current value of this date part to the provided value. + */ + abstract setValue(value: string): void + + /** + * Returns `true` if this `DatePart` is a `Token`, `false` otherwise. + */ + isToken(): this is Token { + return false + } + + /** + * Retrieves the next date part in the list of parts. + */ + nextPart(): Option.Option { + return pipe( + ReadonlyArray.findFirstIndex(this.parts, (part) => part === this), + Option.flatMap((currentPartIndex) => + ReadonlyArray.findFirst(this.parts.slice(currentPartIndex + 1), (part) => !part.isToken()) + ) + ) + } + + /** + * Retrieves the previous date part in the list of parts. + */ + previousPart(): Option.Option { + return pipe( + ReadonlyArray.findFirstIndex(this.parts, (part) => part === this), + Option.flatMap((currentPartIndex) => + ReadonlyArray.findLast(this.parts.slice(0, currentPartIndex), (part) => !part.isToken()) + ) + ) + } + + toString() { + return String(this.date) + } +} + +class Token extends DatePart { + increment(): void {} + + decrement(): void {} + + setValue(value: string): void { + this.token = this.token + value + } + + isToken(): this is Token { + return true + } + + toString() { + return this.token + } +} + +class Milliseconds extends DatePart { + increment(): void { + this.date.setMilliseconds(this.date.getMilliseconds() + 1) + } + + decrement(): void { + this.date.setMilliseconds(this.date.getMilliseconds() - 1) + } + + setValue(value: string): void { + this.date.setMilliseconds(Number.parseInt(value.slice(-this.token.length))) + } + + toString() { + const millis = `${this.date.getMilliseconds()}` + return millis.padStart(4, "0").substring(0, this.token.length) + } +} + +class Seconds extends DatePart { + increment(): void { + this.date.setSeconds(this.date.getSeconds() + 1) + } + + decrement(): void { + this.date.setSeconds(this.date.getSeconds() - 1) + } + + setValue(value: string): void { + this.date.setSeconds(Number.parseInt(value.slice(-2))) + } + + toString() { + const seconds = `${this.date.getSeconds()}` + return this.token.length > 1 + ? seconds.padStart(2, "0") + : seconds + } +} + +class Minutes extends DatePart { + increment(): void { + this.date.setMinutes(this.date.getMinutes() + 1) + } + + decrement(): void { + this.date.setMinutes(this.date.getMinutes() - 1) + } + + setValue(value: string): void { + this.date.setMinutes(Number.parseInt(value.slice(-2))) + } + + toString() { + const minutes = `${this.date.getMinutes()}` + return this.token.length > 1 + ? minutes.padStart(2, "0") : + minutes + } +} + +class Hours extends DatePart { + increment(): void { + this.date.setHours(this.date.getHours() + 1) + } + + decrement(): void { + this.date.setHours(this.date.getHours() - 1) + } + + setValue(value: string): void { + this.date.setHours(Number.parseInt(value.slice(-2))) + } + + toString() { + const hours = /h/.test(this.token) + ? this.date.getHours() % 12 || 12 + : this.date.getHours() + return this.token.length > 1 + ? `${hours}`.padStart(2, "0") + : `${hours}` + } +} + +class Day extends DatePart { + increment(): void { + this.date.setDate(this.date.getDate() + 1) + } + + decrement(): void { + this.date.setDate(this.date.getDate() - 1) + } + + setValue(value: string): void { + this.date.setDate(Number.parseInt(value.slice(-2))) + } + + toString() { + const date = this.date.getDate() + const day = this.date.getDay() + return pipe( + Match.value(this.token), + Match.when("DD", () => `${date}`.padStart(2, "0")), + Match.when("Do", () => `${date}${this.ordinalIndicator(date)}`), + Match.when("d", () => `${day + 1}`), + Match.when("ddd", () => this.locales!.weekdaysShort[day]!), + Match.when("dddd", () => this.locales!.weekdays[day]!), + Match.orElse(() => `${date}`) + ) + } + + private ordinalIndicator(day: number): string { + return pipe( + Match.value(day % 10), + Match.when(1, () => "st"), + Match.when(2, () => "nd"), + Match.when(3, () => "rd"), + Match.orElse(() => "th") + ) + } +} + +class Month extends DatePart { + increment(): void { + this.date.setMonth(this.date.getMonth() + 1) + } + + decrement(): void { + this.date.setMonth(this.date.getMonth() - 1) + } + + setValue(value: string): void { + const month = Number.parseInt(value.slice(-2)) - 1 + this.date.setMonth(month < 0 ? 0 : month) + } + + toString() { + const month = this.date.getMonth() + return pipe( + Match.value(this.token.length), + Match.when(2, () => `${month + 1}`.padStart(2, "0")), + Match.when(3, () => this.locales!.monthsShort[month]!), + Match.when(4, () => this.locales!.months[month]!), + Match.orElse(() => `${month + 1}`) + ) + } +} + +class Year extends DatePart { + increment(): void { + this.date.setFullYear(this.date.getFullYear() + 1) + } + + decrement(): void { + this.date.setFullYear(this.date.getFullYear() - 1) + } + + setValue(value: string): void { + this.date.setFullYear(Number.parseInt(value.slice(-4))) + } + + toString() { + const year = `${this.date.getFullYear()}`.padStart(4, "0") + return this.token.length === 2 + ? year.substring(-2) + : year + } +} + +class Meridiem extends DatePart { + increment(): void { + this.date.setHours((this.date.getHours() + 12) % 24) + } + + decrement(): void { + this.increment() + } + + setValue(_value: string): void {} + + toString() { + const meridiem = this.date.getHours() > 12 ? "pm" : "am" + return /A/.test(this.token) + ? meridiem.toUpperCase() + : meridiem + } +} diff --git a/src/internal/prompt/list.ts b/src/internal/prompt/list.ts new file mode 100644 index 0000000..2c6f1e2 --- /dev/null +++ b/src/internal/prompt/list.ts @@ -0,0 +1,9 @@ +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalTextPrompt from "./text.js" + +/** @internal */ +export const list = (options: Prompt.Prompt.ListOptions): Prompt.Prompt> => + InternalTextPrompt.text(options).pipe( + InternalPrompt.map((output) => output.split(options.delimiter || ",")) + ) diff --git a/src/internal/prompt/number.ts b/src/internal/prompt/number.ts index d316e30..229188f 100644 --- a/src/internal/prompt/number.ts +++ b/src/internal/prompt/number.ts @@ -1,19 +1,25 @@ +import * as Terminal from "@effect/platform/Terminal" import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" import * as AnsiRender from "@effect/printer-ansi/AnsiRender" import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" import * as Color from "@effect/printer-ansi/Color" import * as Doc from "@effect/printer/Doc" import * as Optimize from "@effect/printer/Optimize" -import { Effect, pipe } from "effect" +import * as Schema from "@effect/schema/Schema" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" import type * as Prompt from "../../Prompt.js" import type * as PromptAction from "../../Prompt/Action.js" -import * as prompt from "../prompt.js" -import * as promptAction from "./action.js" -import * as ansiUtils from "./ansi-utils.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalPromptAction from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" interface State { readonly cursor: number readonly value: string + readonly error: Option.Option } const round = (number: number, precision: number) => { @@ -21,92 +27,153 @@ const round = (number: number, precision: number) => { return Math.round(number * factor) / factor } -const parseInt = (value: string): Effect.Effect => - Effect.suspend(() => { - const parsed = Number.parseInt(value) - if (Number.isNaN(parsed)) { - return Effect.fail(void 0) - } - return Effect.succeed(parsed) +const parseInt = Schema.NumberFromString.pipe( + Schema.int(), + Schema.parse +) + +const parseFloat = Schema.parse(Schema.NumberFromString) + +const renderBeep = AnsiRender.prettyDefault(InternalAnsiUtils.beep) + +const renderClearScreen = ( + prevState: Option.Option, + options: Required, + columns: number +): AnsiDoc.AnsiDoc => { + // Erase the main prompt line and place the cursor in column one + const clearPrompt = Doc.cat(InternalAnsiUtils.eraseLine, InternalAnsiUtils.cursorLeft) + // If there is no previous state, then this is the first render, so there is + // no need to clear the error output or any previous prompt output - we can + // just clear the current line for the prompt + if (Option.isNone(prevState)) { + return clearPrompt + } + // If there was a previous state, check for any error output + const clearError = Option.match(prevState.value.error, { + onNone: () => Doc.empty, + onSome: (error) => + // If there was an error, move the cursor down to the final error line and + // then clear all lines of error output + pipe( + InternalAnsiUtils.cursorDown(InternalAnsiUtils.lines(error, columns)), + Doc.cat(InternalAnsiUtils.eraseText(`\n${error}`, columns)) + ) }) + // Ensure that the prior prompt output is cleaned up + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + // Concatenate and return all documents + return Doc.cat(clearError, Doc.cat(clearOutput, clearPrompt)) +} -const parseFloat = (value: string): Effect.Effect => - Effect.suspend(() => { - const parsed = Number.parseFloat(value) - if (Number.isNaN(parsed)) { - return Effect.fail(void 0) - } - return Effect.succeed(parsed) +const renderInput = (nextState: State): AnsiDoc.AnsiDoc => { + const annotation = Option.match(nextState.error, { + onNone: () => AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.green)), + onSome: () => AnsiStyle.color(Color.red) }) + const value = nextState.value === "" ? Doc.empty : Doc.text(`${nextState.value}`) + return Doc.annotate(value, annotation) +} -const renderBeep = AnsiRender.prettyDefault(ansiUtils.beep) +const renderError = (nextState: State, pointer: AnsiDoc.AnsiDoc): AnsiDoc.AnsiDoc => + Option.match(nextState.error, { + onNone: () => Doc.empty, + onSome: (error) => { + const errorLines = error.split(/\r?\n/) + if (ReadonlyArray.isNonEmptyReadonlyArray(errorLines)) { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate( + Doc.text(line), + AnsiStyle.combine(AnsiStyle.italicized, AnsiStyle.color(Color.red)) + ) + const prefix = Doc.cat(Doc.annotate(pointer, AnsiStyle.color(Color.red)), Doc.space) + const lines = ReadonlyArray.map(errorLines, (str) => annotateLine(str)) + return pipe( + InternalAnsiUtils.cursorSave, + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(InternalAnsiUtils.cursorRestore) + ) + } + return Doc.empty + } + }) -const renderError = (promptMsg: string, errorMsg: string, input: AnsiDoc.AnsiDoc) => - Effect.map(ansiUtils.figures, ({ pointerSmall }) => { - const doc = pipe( - ansiUtils.resetLine, - Doc.cat(Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), +const renderOutput = ( + nextState: State, + leadingSymbol: AnsiDoc.AnsiDoc, + trailingSymbol: AnsiDoc.AnsiDoc, + options: Required +): AnsiDoc.AnsiDoc => { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate(Doc.text(line), AnsiStyle.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (ReadonlyArray.isNonEmptyReadonlyArray(promptLines)) { + const lines = ReadonlyArray.map(promptLines, (line) => annotateLine(line)) + return pipe( + prefix, + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), Doc.cat(Doc.space), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.black))), + Doc.cat(trailingSymbol), Doc.cat(Doc.space), - Doc.cat( - Doc.annotate(input, AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.red))) - ), - Doc.cat(ansiUtils.cursorSave), - Doc.cat(Doc.hardLine), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.red))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate( - Doc.text(errorMsg), - AnsiStyle.combine(AnsiStyle.italicized, AnsiStyle.color(Color.red)) - )), - Doc.cat(ansiUtils.cursorRestore) + Doc.cat(renderInput(nextState)) ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) - }) + } + return Doc.hsep([prefix, trailingSymbol, renderInput(nextState)]) +} -const renderNextFrame = (promptMsg: string, input: AnsiDoc.AnsiDoc) => - Effect.map(ansiUtils.figures, ({ pointerSmall }) => { - const doc = pipe( - ansiUtils.resetDown, - Doc.cat(Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.black))), - Doc.cat(Doc.space), - Doc.cat( - Doc.annotate(input, AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.green))) - ) +const renderNextFrame = ( + prevState: Option.Option, + nextState: State, + options: Required +) => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const leadingSymbol = Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan)) + const trailingSymbol = Doc.annotate(figures.pointerSmall, AnsiStyle.color(Color.black)) + const clearScreen = renderClearScreen(prevState, options, terminal.columns) + const errorMsg = renderError(nextState, figures.pointerSmall) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(errorMsg), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) }) -const renderSubmission = (promptMsg: string, input: AnsiDoc.AnsiDoc) => - Effect.map(ansiUtils.figures, ({ ellipsis, tick }) => { - const doc = pipe( - ansiUtils.resetDown, - Doc.cat(Doc.annotate(tick, AnsiStyle.color(Color.green))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(ellipsis, AnsiStyle.color(Color.black))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(input, AnsiStyle.color(Color.white))), - Doc.cat(Doc.hardLine) +const renderSubmission = ( + nextState: State, + options: Required +) => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(Option.some(nextState), options, terminal.columns) + const leadingSymbol = Doc.annotate(figures.tick, AnsiStyle.color(Color.green)) + const trailingSymbol = Doc.annotate(figures.ellipsis, AnsiStyle.color(Color.black)) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) }) const processBackspace = (currentState: State) => { if (currentState.value.length <= 0) { - return Effect.succeed(promptAction.beep) + return Effect.succeed(InternalPromptAction.beep) } - return Effect.succeed(promptAction.nextFrame({ + return Effect.succeed(InternalPromptAction.nextFrame({ ...currentState, - value: currentState.value.slice(0, currentState.value.length - 1) + value: currentState.value.slice(0, currentState.value.length - 1), + error: Option.none() })) } @@ -115,11 +182,20 @@ const defaultIntProcessor = ( input: string ): Effect.Effect> => { if (currentState.value.length === 0 && input === "-") { - return Effect.succeed(promptAction.nextFrame({ ...currentState, value: "-" })) + return Effect.succeed(InternalPromptAction.nextFrame({ + ...currentState, + value: "-", + error: Option.none() + })) } return Effect.match(parseInt(currentState.value + input), { - onFailure: () => promptAction.beep, - onSuccess: (value) => promptAction.nextFrame({ ...currentState, value: `${value}` }) + onFailure: () => InternalPromptAction.beep, + onSuccess: (value) => + InternalPromptAction.nextFrame({ + ...currentState, + value: `${value}`, + error: Option.none() + }) }) } @@ -128,22 +204,31 @@ const defaultFloatProcessor = ( input: string ): Effect.Effect> => { if (input === "." && currentState.value.includes(".")) { - return Effect.succeed(promptAction.beep) + return Effect.succeed(InternalPromptAction.beep) } if (currentState.value.length === 0 && input === "-") { - return Effect.succeed(promptAction.nextFrame({ ...currentState, value: "-" })) + return Effect.succeed(InternalPromptAction.nextFrame({ + ...currentState, + value: "-", + error: Option.none() + })) } return Effect.match(parseFloat(currentState.value + input), { - onFailure: () => promptAction.beep, + onFailure: () => InternalPromptAction.beep, onSuccess: (value) => - promptAction.nextFrame({ + InternalPromptAction.nextFrame({ ...currentState, - value: input === "." ? `${value}.` : `${value}` + value: input === "." ? `${value}.` : `${value}`, + error: Option.none() }) }) } -const initialState: State = { cursor: 0, value: "" } +const initialState: State = { + cursor: 0, + value: "", + error: Option.none() +} /** @internal */ export const integer = (options: Prompt.Prompt.IntegerOptions): Prompt.Prompt => { @@ -163,62 +248,72 @@ export const integer = (options: Prompt.Prompt.IntegerOptions): Prompt.Prompt { - const input = state.value === "" ? Doc.empty : Doc.text(`${state.value}`) + (prevState, nextState, action) => { switch (action._tag) { case "Beep": { return Effect.succeed(renderBeep) } - case "Error": { - return renderError(opts.message, action.message, input) - } case "NextFrame": { - return renderNextFrame(opts.message, input) + return renderNextFrame(prevState, nextState, opts) } case "Submit": { - return renderSubmission(opts.message, input) + return renderSubmission(nextState, opts) } } }, (input, state) => { - switch (input.action) { - case "Backspace": { + switch (input.key.name) { + case "backspace": { return processBackspace(state) } - case "CursorUp": { + case "k": + case "up": { return Effect.sync(() => - promptAction.nextFrame({ + InternalPromptAction.nextFrame({ ...state, value: state.value === "" || state.value === "-" ? `${opts.incrementBy}` - : `${Number.parseInt(state.value) + opts.incrementBy}` + : `${Number.parseInt(state.value) + opts.incrementBy}`, + error: Option.none() }) ) } - case "CursorDown": { + case "j": + case "down": { return Effect.sync(() => - promptAction.nextFrame({ + InternalPromptAction.nextFrame({ ...state, value: state.value === "" || state.value === "-" ? `-${opts.decrementBy}` - : `${Number.parseInt(state.value) - opts.decrementBy}` + : `${Number.parseInt(state.value) - opts.decrementBy}`, + error: Option.none() }) ) } - case "Submit": { + case "enter": + case "return": { return Effect.matchEffect(parseInt(state.value), { - onFailure: () => Effect.succeed(promptAction.error("Must provide an integer value")), + onFailure: () => + Effect.succeed(InternalPromptAction.nextFrame({ + ...state, + error: Option.some("Must provide an integer value") + })), onSuccess: (n) => Effect.match(opts.validate(n), { - onFailure: promptAction.error, - onSuccess: promptAction.submit + onFailure: (error) => + InternalPromptAction.nextFrame({ + ...state, + error: Option.some(error) + }), + onSuccess: InternalPromptAction.submit }) }) } default: { - return defaultIntProcessor(state, input.value) + const value = Option.getOrElse(input.input, () => "") + return defaultIntProcessor(state, value) } } } @@ -244,67 +339,76 @@ export const float = (options: Prompt.Prompt.FloatOptions): Prompt.Prompt { - const input = state.value === "" ? Doc.empty : Doc.text(`${state.value}`) + (prevState, nextState, action) => { switch (action._tag) { case "Beep": { return Effect.succeed(renderBeep) } - case "Error": { - return renderError(opts.message, action.message, input) - } case "NextFrame": { - return renderNextFrame(opts.message, input) + return renderNextFrame(prevState, nextState, opts) } case "Submit": { - return renderSubmission(opts.message, input) + return renderSubmission(nextState, opts) } } }, (input, state) => { - switch (input.action) { - case "Backspace": { + switch (input.key.name) { + case "backspace": { return processBackspace(state) } - case "CursorUp": { + case "k": + case "up": { return Effect.sync(() => - promptAction.nextFrame({ + InternalPromptAction.nextFrame({ ...state, value: state.value === "" || state.value === "-" ? `${opts.incrementBy}` - : `${Number.parseFloat(state.value) + opts.incrementBy}` + : `${Number.parseFloat(state.value) + opts.incrementBy}`, + error: Option.none() }) ) } - case "CursorDown": { + case "j": + case "down": { return Effect.sync(() => - promptAction.nextFrame({ + InternalPromptAction.nextFrame({ ...state, value: state.value === "" || state.value === "-" ? `-${opts.decrementBy}` - : `${Number.parseFloat(state.value) - opts.decrementBy}` + : `${Number.parseFloat(state.value) - opts.decrementBy}`, + error: Option.none() }) ) } - case "Submit": { + case "enter": + case "return": { return Effect.matchEffect(parseFloat(state.value), { onFailure: () => - Effect.succeed(promptAction.error("Must provide a floating point value")), + Effect.succeed(InternalPromptAction.nextFrame({ + ...state, + error: Option.some("Must provide a floating point value") + })), onSuccess: (n) => Effect.flatMap( Effect.sync(() => round(n, opts.precision)), (rounded) => Effect.match(opts.validate(rounded), { - onFailure: promptAction.error, - onSuccess: promptAction.submit + onFailure: (error) => + InternalPromptAction.nextFrame({ + ...state, + error: Option.some(error) + }), + onSuccess: InternalPromptAction.submit }) ) }) } default: { - return defaultFloatProcessor(state, input.value) + const value = Option.getOrElse(input.input, () => "") + return defaultFloatProcessor(state, value) } } } diff --git a/src/internal/prompt/select.ts b/src/internal/prompt/select.ts index e2ede95..6f4bea6 100644 --- a/src/internal/prompt/select.ts +++ b/src/internal/prompt/select.ts @@ -1,159 +1,249 @@ +import * as Terminal from "@effect/platform/Terminal" +import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" import * as AnsiRender from "@effect/printer-ansi/AnsiRender" import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" import * as Color from "@effect/printer-ansi/Color" import * as Doc from "@effect/printer/Doc" -import * as Optimize from "@effect/printer/Optimize" -import { Effect, pipe } from "effect" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" import type * as Prompt from "../../Prompt.js" -import * as prompt from "../prompt.js" -import * as promptAction from "./action.js" -import * as ansiUtils from "./ansi-utils.js" -import * as utils from "./utils.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalPromptAction from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" +import { entriesToDisplay } from "./utils.js" interface State { readonly cursor: number } -const renderBeep = AnsiRender.prettyDefault(ansiUtils.beep) +const renderBeep = AnsiRender.prettyDefault(InternalAnsiUtils.beep) -const renderChoices = ( - state: State, - choices: Prompt.Prompt.SelectOptions["choices"], - pointer: Doc.Doc -) => { - const { endIndex, startIndex } = utils.displayRange(state.cursor, choices.length) - const choicesToRender = choices.slice(startIndex, endIndex) - const selectedStyle = AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.green)) - const docs = choicesToRender.map(({ description, title }, index) => { - const isSelected = state.cursor === index - const prefix = isSelected ? Doc.annotate(pointer, AnsiStyle.color(Color.green)) : Doc.space - const doc = isSelected ? Doc.annotate(Doc.text(title), selectedStyle) : Doc.text(title) - if (description !== undefined && isSelected) { - const desc = pipe( - Doc.char("-"), - Doc.cat(Doc.space), - Doc.cat(Doc.text(description)), - Doc.annotate(AnsiStyle.color(Color.black)) - ) - return pipe( - prefix, - Doc.cat(Doc.space), - Doc.cat(doc), - Doc.cat(Doc.space), - Doc.cat(desc) +const renderClearScreen = ( + prevState: Option.Option, + options: Prompt.Prompt.SelectOptions, + columns: number +): AnsiDoc.AnsiDoc => { + const clearPrompt = Doc.cat(InternalAnsiUtils.eraseLine, InternalAnsiUtils.cursorLeft) + if (Option.isNone(prevState)) { + return clearPrompt + } + const text = "\n".repeat(options.choices.length) + options.message + const clearOutput = InternalAnsiUtils.eraseText(text, columns) + return Doc.cat(clearOutput, clearPrompt) +} + +const renderChoicePrefix = ( + nextState: State, + choices: Prompt.Prompt.SelectOptions["choices"], + toDisplay: { readonly startIndex: number; readonly endIndex: number }, + currentIndex: number, + figures: Effect.Effect.Success +): AnsiDoc.AnsiDoc => { + let prefix: AnsiDoc.AnsiDoc = Doc.space + if (currentIndex === toDisplay.startIndex && toDisplay.startIndex > 0) { + prefix = figures.arrowUp + } else if (currentIndex === toDisplay.endIndex - 1 && toDisplay.endIndex < choices.length) { + prefix = figures.arrowDown + } + if (choices[currentIndex]!.disabled) { + const annotation = AnsiStyle.combine(AnsiStyle.bold, AnsiStyle.color(Color.black)) + return nextState.cursor === currentIndex + ? pipe( + Doc.annotate(figures.pointer, annotation), + Doc.cat(prefix) ) - } + : Doc.cat(Doc.space, prefix) + } + return nextState.cursor === currentIndex + ? pipe( + Doc.annotate(figures.pointer, AnsiStyle.color(Color.green)), + Doc.cat(prefix) + ) + : Doc.cat(Doc.space, prefix) +} + +const renderChoiceTitle = ( + choice: Prompt.Prompt.SelectChoice, + isSelected: boolean +): AnsiDoc.AnsiDoc => { + const title = Doc.text(choice.title) + const blackUnderlined = AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.black)) + const greenUnderlined = AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.green)) + if (isSelected) { + return choice.disabled + ? Doc.annotate(title, blackUnderlined) + : Doc.annotate(title, greenUnderlined) + } + return choice.disabled + // TODO: strikethrough in printer? + ? Doc.annotate(title, blackUnderlined) + : title +} + +const renderChoiceDescription = ( + choice: Prompt.Prompt.SelectChoice, + isSelected: boolean +): AnsiDoc.AnsiDoc => { + if (!choice.disabled && choice.description && isSelected) { return pipe( - prefix, + Doc.char("-"), Doc.cat(Doc.space), - Doc.cat(doc) + Doc.cat(Doc.text(choice.description)), + Doc.annotate(AnsiStyle.color(Color.black)) ) + } + return Doc.empty +} + +const renderChoices = ( + nextState: State, + options: Prompt.Prompt.SelectOptions, + figures: Effect.Effect.Success +): AnsiDoc.AnsiDoc => { + const choices = options.choices + const toDisplay = entriesToDisplay(nextState.cursor, choices.length, options.maxPerPage) + const choicesToRender = choices.slice(toDisplay.startIndex, toDisplay.endIndex) + const docs = ReadonlyArray.map(choicesToRender, (choice, currentIndex) => { + const prefix = renderChoicePrefix(nextState, choicesToRender, toDisplay, currentIndex, figures) + const title = renderChoiceTitle(choice, nextState.cursor === currentIndex) + const description = renderChoiceDescription(choice, nextState.cursor === currentIndex) + return pipe(prefix, Doc.cat(title), Doc.cat(Doc.space), Doc.cat(description)) }) return Doc.vsep(docs) } -const renderNextFrame = ( - promptMsg: string, - state: State, - choices: Prompt.Prompt.SelectOptions["choices"] -) => - Effect.map(ansiUtils.figures, ({ pointer, pointerSmall }) => { - const renderedChoices = renderChoices(state, choices, pointer) - const doc = pipe( - ansiUtils.setCursorPosition(0), - Doc.cat(ansiUtils.clearLines(choices.length + 1)), - Doc.cat(ansiUtils.cursorHide), - Doc.cat(Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), +const renderOutput = ( + leadingSymbol: AnsiDoc.AnsiDoc, + trailingSymbol: AnsiDoc.AnsiDoc, + options: Required> +): AnsiDoc.AnsiDoc => { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate(Doc.text(line), AnsiStyle.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (ReadonlyArray.isNonEmptyReadonlyArray(promptLines)) { + const lines = ReadonlyArray.map(promptLines, (line) => annotateLine(line)) + return pipe( + prefix, + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), Doc.cat(Doc.space), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.black))), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space) + ) + } + return Doc.hsep([prefix, trailingSymbol]) +} + +const renderNextFrame = ( + prevState: Option.Option, + nextState: State, + options: Required> +): Effect.Effect => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const choices = renderChoices(nextState, options, figures) + const clearScreen = renderClearScreen(prevState, options, terminal.columns) + const leadingSymbol = Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan)) + const trailingSymbol = Doc.annotate(figures.pointerSmall, AnsiStyle.color(Color.black)) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(InternalAnsiUtils.cursorHide), + Doc.cat(promptMsg), Doc.cat(Doc.hardLine), - Doc.cat(renderedChoices) + Doc.cat(choices), + // TODO: figure out what the bug is here that screws up formatting + // Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) }) -const renderSubmission = ( - promptMsg: string, +const renderSubmission = ( state: State, - choices: Prompt.Prompt.SelectOptions["choices"] + options: Required> ) => - Effect.map(ansiUtils.figures, ({ ellipsis, tick }) => { - const selected = Doc.text(choices[state.cursor].title) - const doc = pipe( - ansiUtils.setCursorPosition(0), - Doc.cat(ansiUtils.clearLines(choices.length + 1)), - Doc.cat(Doc.annotate(tick, AnsiStyle.color(Color.green))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(ellipsis, AnsiStyle.color(Color.black))), + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const selected = Doc.text(options.choices[state.cursor].title) + const clearScreen = renderClearScreen(Option.some(state), options, terminal.columns) + const leadingSymbol = Doc.annotate(figures.tick, AnsiStyle.color(Color.green)) + const trailingSymbol = Doc.annotate(figures.ellipsis, AnsiStyle.color(Color.black)) + const promptMsg = renderOutput(leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), Doc.cat(Doc.space), Doc.cat(Doc.annotate(selected, AnsiStyle.color(Color.white))), - Doc.cat(Doc.hardLine) + Doc.cat(Doc.hardLine), + // TODO: figure out what the bug is here that screws up formatting + // Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) }) const initialState: State = { cursor: 0 } -const processCursorUp = (state: State, choices: Prompt.Prompt.SelectOptions["choices"]) => { +const processCursorUp = (state: State, choices: Prompt.Prompt.SelectOptions["choices"]) => { if (state.cursor === 0) { - return Effect.succeed(promptAction.nextFrame({ cursor: choices.length - 1 })) + return Effect.succeed(InternalPromptAction.nextFrame({ cursor: choices.length - 1 })) } - return Effect.succeed(promptAction.nextFrame({ cursor: state.cursor - 1 })) + return Effect.succeed(InternalPromptAction.nextFrame({ cursor: state.cursor - 1 })) } -const processCursorDown = (state: State, choices: Prompt.Prompt.SelectOptions["choices"]) => { +const processCursorDown = (state: State, choices: Prompt.Prompt.SelectOptions["choices"]) => { if (state.cursor === choices.length - 1) { - return Effect.succeed(promptAction.nextFrame({ cursor: 0 })) + return Effect.succeed(InternalPromptAction.nextFrame({ cursor: 0 })) } - return Effect.succeed(promptAction.nextFrame({ cursor: state.cursor + 1 })) + return Effect.succeed(InternalPromptAction.nextFrame({ cursor: state.cursor + 1 })) } -const processNext = (state: State, choices: Prompt.Prompt.SelectOptions["choices"]) => - Effect.succeed(promptAction.nextFrame({ cursor: (state.cursor + 1) % choices.length })) +const processNext = (state: State, choices: Prompt.Prompt.SelectOptions["choices"]) => + Effect.succeed(InternalPromptAction.nextFrame({ cursor: (state.cursor + 1) % choices.length })) /** @internal */ -export const select = (options: Prompt.Prompt.SelectOptions): Prompt.Prompt => { - const opts: Required = { +export const select = (options: Prompt.Prompt.SelectOptions): Prompt.Prompt => { + const opts: Required> = { + maxPerPage: 10, ...options } - return prompt.custom( + return InternalPrompt.custom( initialState, - (state, action) => { + (prevState, nextState, action) => { switch (action._tag) { case "Beep": { return Effect.succeed(renderBeep) } - case "Error": { - return Effect.succeed(renderBeep) - } case "NextFrame": { - return renderNextFrame(opts.message, state, opts.choices) + return renderNextFrame(prevState, nextState, opts) } case "Submit": { - return renderSubmission(opts.message, state, opts.choices) + return renderSubmission(nextState, opts) } } }, (input, state) => { - switch (input.action) { - case "CursorUp": { + switch (input.key.name) { + case "k": + case "up": { return processCursorUp(state, opts.choices) } - case "CursorDown": { + case "j": + case "down": { return processCursorDown(state, opts.choices) } - case "Next": { + case "tab": { return processNext(state, opts.choices) } - case "Submit": { - return Effect.succeed(promptAction.submit(opts.choices[state.cursor].value)) + case "enter": + case "return": { + return Effect.succeed(InternalPromptAction.submit(opts.choices[state.cursor].value)) } default: { - return Effect.succeed(promptAction.beep) + return Effect.succeed(InternalPromptAction.beep) } } } diff --git a/src/internal/prompt/text.ts b/src/internal/prompt/text.ts index 8a15c99..129fddd 100644 --- a/src/internal/prompt/text.ts +++ b/src/internal/prompt/text.ts @@ -1,126 +1,217 @@ +import * as Terminal from "@effect/platform/Terminal" import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" import * as AnsiRender from "@effect/printer-ansi/AnsiRender" import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" import * as Color from "@effect/printer-ansi/Color" import * as Doc from "@effect/printer/Doc" import * as Optimize from "@effect/printer/Optimize" -import { Effect, pipe } from "effect" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" import type * as Prompt from "../../Prompt.js" -import * as prompt from "../prompt.js" -import * as promptAction from "./action.js" -import * as ansiUtils from "./ansi-utils.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalPromptAction from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" interface State { readonly cursor: number readonly offset: number readonly value: string + readonly error: Option.Option } -const renderBeep = AnsiRender.prettyDefault(ansiUtils.beep) +const renderBeep = AnsiRender.prettyDefault(InternalAnsiUtils.beep) -const renderError = (promptMsg: string, errorMsg: string, input: AnsiDoc.AnsiDoc, offset: number) => - Effect.map(ansiUtils.figures, ({ pointerSmall }) => { - const doc = pipe( - ansiUtils.resetLine, - Doc.cat(Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.black))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(input, AnsiStyle.color(Color.red))), - Doc.cat(ansiUtils.cursorSave), - Doc.cat(Doc.hardLine), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.red))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate( - Doc.text(errorMsg), - AnsiStyle.combine(AnsiStyle.italicized, AnsiStyle.color(Color.red)) - )), - Doc.cat(ansiUtils.cursorRestore), - Doc.cat(ansiUtils.moveCursor(offset)) - ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) - }) - -const renderNextFrame = (promptMsg: string, input: AnsiDoc.AnsiDoc, offset: number) => - Effect.map(ansiUtils.figures, ({ pointerSmall }) => { - const doc = pipe( - ansiUtils.resetDown, - Doc.cat(Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(pointerSmall, AnsiStyle.color(Color.black))), - Doc.cat(Doc.space), - Doc.cat( - Doc.annotate(input, AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.green))) - ), - Doc.cat(ansiUtils.moveCursor(offset)) - ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) - }) - -const renderSubmission = (promptMsg: string, input: AnsiDoc.AnsiDoc) => - Effect.map(ansiUtils.figures, ({ ellipsis, tick }) => { - const doc = pipe( - ansiUtils.resetDown, - Doc.cat(Doc.annotate(tick, AnsiStyle.color(Color.green))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(Doc.text(promptMsg), AnsiStyle.bold)), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(ellipsis, AnsiStyle.color(Color.black))), - Doc.cat(Doc.space), - Doc.cat(Doc.annotate(input, AnsiStyle.color(Color.white))), - Doc.cat(Doc.hardLine) - ) - return AnsiRender.prettyDefault(Optimize.optimize(doc, Optimize.Deep)) +const renderClearScreen = ( + prevState: Option.Option, + options: Required, + columns: number +): AnsiDoc.AnsiDoc => { + // Erase the main prompt line and place the cursor in column one + const clearPrompt = Doc.cat(InternalAnsiUtils.eraseLine, InternalAnsiUtils.cursorLeft) + // If there is no previous state, then this is the first render, so there is + // no need to clear the error output or any previous prompt output - we can + // just clear the current line for the prompt + if (Option.isNone(prevState)) { + return clearPrompt + } + // If there was a previous state, check for any error output + const clearError = Option.match(prevState.value.error, { + onNone: () => Doc.empty, + onSome: (error) => + // If there was an error, move the cursor down to the final error line and + // then clear all lines of error output + pipe( + InternalAnsiUtils.cursorDown(InternalAnsiUtils.lines(error, columns)), + Doc.cat(InternalAnsiUtils.eraseText(`\n${error}`, columns)) + ) }) + // Ensure that the prior prompt output is cleaned up + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + // Concatenate and return all documents + return Doc.cat(clearError, Doc.cat(clearOutput, clearPrompt)) +} const renderInput = ( - value: string, - type: NonNullable + nextState: State, + options: Required, + submitted: boolean = false ): AnsiDoc.AnsiDoc => { - switch (type) { + const annotation = Option.match(nextState.error, { + onNone: () => + submitted + ? AnsiStyle.color(Color.white) + : AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.green)), + onSome: () => AnsiStyle.color(Color.red) + }) + switch (options.type) { case "hidden": { return Doc.empty } case "password": { - return Doc.text("*".repeat(value.length)) + return Doc.annotate(Doc.text("*".repeat(nextState.value.length)), annotation) } case "text": { - return Doc.text(value) + return Doc.annotate(Doc.text(nextState.value), annotation) } } } +const renderError = (nextState: State, pointer: AnsiDoc.AnsiDoc): AnsiDoc.AnsiDoc => + Option.match(nextState.error, { + onNone: () => Doc.empty, + onSome: (error) => { + const errorLines = error.split(/\r?\n/) + if (ReadonlyArray.isNonEmptyReadonlyArray(errorLines)) { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate( + Doc.text(line), + AnsiStyle.combine(AnsiStyle.italicized, AnsiStyle.color(Color.red)) + ) + const prefix = Doc.cat(Doc.annotate(pointer, AnsiStyle.color(Color.red)), Doc.space) + const lines = ReadonlyArray.map(errorLines, (str) => annotateLine(str)) + return pipe( + InternalAnsiUtils.cursorSave, + Doc.cat(Doc.hardLine), + Doc.cat(prefix), + Doc.cat(Doc.align(Doc.vsep(lines))), + Doc.cat(InternalAnsiUtils.cursorRestore) + ) + } + return Doc.empty + } + }) + +const renderOutput = ( + nextState: State, + leadingSymbol: AnsiDoc.AnsiDoc, + trailingSymbol: AnsiDoc.AnsiDoc, + options: Required +): AnsiDoc.AnsiDoc => { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate(Doc.text(line), AnsiStyle.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (ReadonlyArray.isNonEmptyReadonlyArray(promptLines)) { + const lines = ReadonlyArray.map(promptLines, (line) => annotateLine(line)) + return pipe( + prefix, + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(renderInput(nextState, options)) + ) + } + return Doc.hsep([prefix, trailingSymbol, renderInput(nextState, options)]) +} + +const renderNextFrame = ( + prevState: Option.Option, + nextState: State, + options: Required +): Effect.Effect => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(prevState, options, terminal.columns) + const leadingSymbol = Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan)) + const trailingSymbol = Doc.annotate(figures.pointerSmall, AnsiStyle.color(Color.black)) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, options) + const errorMsg = renderError(nextState, figures.pointerSmall) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(errorMsg), + Doc.cat(InternalAnsiUtils.cursorMove(nextState.offset)), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const renderSubmission = ( + nextState: State, + options: Required +) => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(Option.some(nextState), options, terminal.columns) + const leadingSymbol = Doc.annotate(figures.tick, AnsiStyle.color(Color.green)) + const trailingSymbol = Doc.annotate(figures.ellipsis, AnsiStyle.color(Color.black)) + const promptMsg = renderOutput(nextState, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + const processBackspace = (currentState: State) => { if (currentState.cursor <= 0) { - return Effect.succeed(promptAction.beep) + return Effect.succeed(InternalPromptAction.beep) } const beforeCursor = currentState.value.slice(0, currentState.cursor - 1) const afterCursor = currentState.value.slice(currentState.cursor) const cursor = currentState.cursor - 1 const value = `${beforeCursor}${afterCursor}` - return Effect.succeed(promptAction.nextFrame({ ...currentState, cursor, value })) + return Effect.succeed(InternalPromptAction.nextFrame({ + ...currentState, + cursor, + value, + error: Option.none() + })) } const processCursorLeft = (currentState: State) => { if (currentState.cursor <= 0) { - return Effect.succeed(promptAction.beep) + return Effect.succeed(InternalPromptAction.beep) } const cursor = currentState.cursor - 1 const offset = currentState.offset - 1 - return Effect.succeed(promptAction.nextFrame({ ...currentState, cursor, offset })) + return Effect.succeed(InternalPromptAction.nextFrame({ + ...currentState, + cursor, + offset, + error: Option.none() + })) } const processCursorRight = (currentState: State) => { if (currentState.cursor >= currentState.value.length) { - return Effect.succeed(promptAction.beep) + return Effect.succeed(InternalPromptAction.beep) } const cursor = Math.min(currentState.cursor + 1, currentState.value.length) const offset = Math.min(currentState.offset + 1, currentState.value.length) - return Effect.succeed(promptAction.nextFrame({ ...currentState, cursor, offset })) + return Effect.succeed(InternalPromptAction.nextFrame({ + ...currentState, + cursor, + offset, + error: Option.none() + })) } const defaultProcessor = (input: string, currentState: State) => { @@ -128,10 +219,20 @@ const defaultProcessor = (input: string, currentState: State) => { const afterCursor = currentState.value.slice(currentState.cursor) const value = `${beforeCursor}${input}${afterCursor}` const cursor = beforeCursor.length + 1 - return Effect.succeed(promptAction.nextFrame({ ...currentState, cursor, value })) + return Effect.succeed(InternalPromptAction.nextFrame({ + ...currentState, + cursor, + value, + error: Option.none() + })) } -const initialState: State = { cursor: 0, offset: 0, value: "" } +const initialState: State = { + cursor: 0, + offset: 0, + value: "", + error: Option.none() +} /** @internal */ export const text = (options: Prompt.Prompt.TextOptions): Prompt.Prompt => { @@ -141,44 +242,46 @@ export const text = (options: Prompt.Prompt.TextOptions): Prompt.Prompt validate: Effect.succeed, ...options } - return prompt.custom( + return InternalPrompt.custom( initialState, - (state, action) => { - const input = renderInput(state.value, opts.type) + (prevState, nextState, action) => { switch (action._tag) { case "Beep": { return Effect.succeed(renderBeep) } - case "Error": { - return renderError(opts.message, action.message, input, state.offset) - } case "NextFrame": { - return renderNextFrame(opts.message, input, state.offset) + return renderNextFrame(prevState, nextState, opts) } case "Submit": { - return renderSubmission(opts.message, input) + return renderSubmission(nextState, opts) } } }, (input, state) => { - switch (input.action) { - case "Backspace": { + switch (input.key.name) { + case "backspace": { return processBackspace(state) } - case "CursorLeft": { + case "left": { return processCursorLeft(state) } - case "CursorRight": { + case "right": { return processCursorRight(state) } - case "Submit": { + case "enter": + case "return": { return Effect.match(opts.validate(state.value), { - onFailure: promptAction.error, - onSuccess: promptAction.submit + onFailure: (error) => + InternalPromptAction.nextFrame({ + ...state, + error: Option.some(error) + }), + onSuccess: InternalPromptAction.submit }) } default: { - return defaultProcessor(input.value, state) + const value = Option.getOrElse(input.input, () => "") + return defaultProcessor(value, state) } } } diff --git a/src/internal/prompt/toggle.ts b/src/internal/prompt/toggle.ts new file mode 100644 index 0000000..7348ee7 --- /dev/null +++ b/src/internal/prompt/toggle.ts @@ -0,0 +1,171 @@ +import * as Terminal from "@effect/platform/Terminal" +import type * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" +import * as AnsiRender from "@effect/printer-ansi/AnsiRender" +import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" +import * as Color from "@effect/printer-ansi/Color" +import * as Doc from "@effect/printer/Doc" +import * as Optimize from "@effect/printer/Optimize" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" +import type * as Prompt from "../../Prompt.js" +import * as InternalPrompt from "../prompt.js" +import * as InternalPromptAction from "./action.js" +import * as InternalAnsiUtils from "./ansi-utils.js" + +interface State { + readonly value: boolean +} + +const renderBeep = AnsiRender.prettyDefault(InternalAnsiUtils.beep) + +const renderClearScreen = ( + prevState: Option.Option, + options: Required, + columns: number +): AnsiDoc.AnsiDoc => { + const clearPrompt = Doc.cat(InternalAnsiUtils.eraseLine, InternalAnsiUtils.cursorLeft) + if (Option.isNone(prevState)) { + return clearPrompt + } + const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + return Doc.cat(clearOutput, clearPrompt) +} + +const renderToggle = (value: boolean, options: Required) => { + const separator = Doc.annotate(Doc.char("/"), AnsiStyle.color(Color.black)) + const selectedAnnotation = AnsiStyle.combine(AnsiStyle.underlined, AnsiStyle.color(Color.cyan)) + const inactive = value + ? Doc.text(options.inactive) + : Doc.annotate(Doc.text(options.inactive), selectedAnnotation) + const active = value + ? Doc.annotate(Doc.text(options.active), selectedAnnotation) + : Doc.text(options.active) + return Doc.hsep([active, separator, inactive]) +} + +const renderOutput = ( + toggle: AnsiDoc.AnsiDoc, + leadingSymbol: AnsiDoc.AnsiDoc, + trailingSymbol: AnsiDoc.AnsiDoc, + options: Required +): AnsiDoc.AnsiDoc => { + const annotateLine = (line: string): AnsiDoc.AnsiDoc => + Doc.annotate(Doc.text(line), AnsiStyle.bold) + const promptLines = options.message.split(/\r?\n/) + const prefix = Doc.cat(leadingSymbol, Doc.space) + if (ReadonlyArray.isNonEmptyReadonlyArray(promptLines)) { + const lines = ReadonlyArray.map(promptLines, (line) => annotateLine(line)) + return pipe( + prefix, + Doc.cat(Doc.nest(Doc.vsep(lines), 2)), + Doc.cat(Doc.space), + Doc.cat(trailingSymbol), + Doc.cat(Doc.space), + Doc.cat(toggle) + ) + } + return Doc.hsep([prefix, trailingSymbol, toggle]) +} + +const renderNextFrame = ( + prevState: Option.Option, + nextState: State, + options: Required +): Effect.Effect => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(prevState, options, terminal.columns) + const leadingSymbol = Doc.annotate(Doc.text("?"), AnsiStyle.color(Color.cyan)) + const trailingSymbol = Doc.annotate(figures.pointerSmall, AnsiStyle.color(Color.black)) + const toggle = renderToggle(nextState.value, options) + const promptMsg = renderOutput(toggle, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(InternalAnsiUtils.cursorHide), + Doc.cat(promptMsg), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const renderSubmission = ( + nextState: State, + value: boolean, + options: Required +) => + Effect.gen(function*(_) { + const terminal = yield* _(Terminal.Terminal) + const figures = yield* _(InternalAnsiUtils.figures) + const clearScreen = renderClearScreen(Option.some(nextState), options, terminal.columns) + const leadingSymbol = Doc.annotate(figures.tick, AnsiStyle.color(Color.green)) + const trailingSymbol = Doc.annotate(figures.ellipsis, AnsiStyle.color(Color.black)) + const toggle = renderToggle(value, options) + const promptMsg = renderOutput(toggle, leadingSymbol, trailingSymbol, options) + return pipe( + clearScreen, + Doc.cat(promptMsg), + Doc.cat(Doc.hardLine), + Optimize.optimize(Optimize.Deep), + AnsiRender.prettyDefault + ) + }) + +const activate = Effect.succeed(InternalPromptAction.nextFrame({ value: true })) +const deactivate = Effect.succeed(InternalPromptAction.nextFrame({ value: false })) + +/** @internal */ +export const toggle = (options: Prompt.Prompt.ToggleOptions): Prompt.Prompt => { + const opts: Required = { + initial: false, + active: "on", + inactive: "off", + ...options + } + return InternalPrompt.custom( + { value: opts.initial } as State, + (prevState, nextState, action) => { + switch (action._tag) { + case "Beep": { + return Effect.succeed(renderBeep) + } + case "NextFrame": { + return renderNextFrame(prevState, nextState, opts) + } + case "Submit": { + return renderSubmission(nextState, action.value, opts) + } + } + }, + (input, state) => { + switch (input.key.name) { + case "0": + case "j": + case "delete": + case "right": + case "down": { + return deactivate + } + case "1": + case "k": + case "left": + case "up": { + return activate + } + case " ": + case "tab": { + return state.value ? deactivate : activate + } + case "enter": + case "return": { + return Effect.succeed(InternalPromptAction.submit(state.value)) + } + default: { + return Effect.succeed(InternalPromptAction.beep) + } + } + } + ) +} diff --git a/src/internal/prompt/utils.ts b/src/internal/prompt/utils.ts index c32cd0a..f651a44 100644 --- a/src/internal/prompt/utils.ts +++ b/src/internal/prompt/utils.ts @@ -1,6 +1,6 @@ /** @internal */ -export const displayRange = (cursor: number, total: number, maxVisible?: number) => { - const max = maxVisible || total +export const entriesToDisplay = (cursor: number, total: number, maxVisible?: number) => { + const max = maxVisible === undefined ? total : maxVisible let startIndex = Math.min(total - max, cursor - Math.floor(max / 2)) if (startIndex < 0) { startIndex = 0 diff --git a/src/internal/terminal.ts b/src/internal/terminal.ts deleted file mode 100644 index 96148d9..0000000 --- a/src/internal/terminal.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as Schema from "@effect/schema/Schema" -import * as TreeFormatter from "@effect/schema/TreeFormatter" -import { Context, Effect, Layer, pipe } from "effect" -import * as readline from "node:readline" -import type * as Terminal from "../Terminal.js" - -/** @internal */ -export const Tag = Context.Tag() - -const RawUserInput = Schema.struct({ - key: Schema.struct({ - sequence: Schema.optional(Schema.union(Schema.string, Schema.undefined)), - name: Schema.optional(Schema.union(Schema.string, Schema.undefined)), - ctrl: Schema.optional(Schema.union(Schema.boolean, Schema.undefined)), - meta: Schema.optional(Schema.union(Schema.boolean, Schema.undefined)), - shift: Schema.optional(Schema.union(Schema.boolean, Schema.undefined)) - }), - input: Schema.union(Schema.string, Schema.undefined) -}) - -const parseUserInput = Schema.parse(RawUserInput) - -const ctrlKeyMap: Record> = { - // ctrl+a - a: Effect.succeed("CursorFirst"), - // ctrl+c - c: Effect.dieMessage("Received interrupt signal, aborting..."), - // ctrl+d - d: Effect.dieMessage("Received interrupt signal, aborting..."), - // ctrl+e - e: Effect.succeed("CursorLast"), - // ctrl+g - g: Effect.succeed("Reset") -} - -const keyMap: Record = { - j: "CursorDown", - k: "CursorUp", - backspace: "Backspace", - end: "End", - enter: "Submit", - escape: "Exit", - delete: "Delete", - down: "CursorDown", - home: "Start", - left: "CursorLeft", - pagedown: "NextPage", - pageup: "PreviousPage", - return: "Submit", - right: "CursorRight", - tab: "Next", - up: "CursorUp" -} - -const parseAction = (key: readline.Key): Effect.Effect => { - if (key.name !== undefined) { - if (key.ctrl && key.name in ctrlKeyMap) { - return ctrlKeyMap[key.name] - } - if (key.name in keyMap) { - return Effect.succeed(keyMap[key.name]) - } - } - return Effect.succeed("Retry") -} - -/** @internal */ -export const LiveTerminal: Layer.Layer = Layer.scoped( - Tag, - Effect.gen(function*($) { - const { input, output } = yield* $( - Effect.all({ - input: Effect.sync(() => process.stdin), - output: Effect.sync(() => process.stdout) - }), - Effect.tap( - ({ input }) => - Effect.acquireRelease( - Effect.sync(() => { - const rl = readline.createInterface({ input, escapeCodeTimeout: 50 }) - readline.emitKeypressEvents(input, rl) - if (input.isTTY) { - input.setRawMode(true) - } - return rl - }), - (rl) => - Effect.sync(() => { - if (input.isTTY) { - input.setRawMode(false) - } - rl.close() - }) - ) - ) - ) - - const getUserInput = Effect.async((resume) => { - const handleKeypress = (input: string | undefined, key: readline.Key) => { - resume(pipe( - parseUserInput({ key, input }), - Effect.mapError((error) => TreeFormatter.formatErrors(error.errors)), - Effect.flatMap(({ input, key }) => - Effect.map( - parseAction(key), - (action) => ({ value: input || "", action }) - ) - ), - Effect.orDie - )) - } - input.once("keypress", handleKeypress) - return Effect.sync(() => { - input.removeListener("keypress", handleKeypress) - }) - }) - - const display = (prompt: string) => - Effect.uninterruptible( - Effect.async((resume) => { - output.write(prompt, () => { - resume(Effect.unit) - }) - }) - ) - - return Tag.of({ getUserInput, display }) - }) -) diff --git a/test/Command.test.ts b/test/Command.test.ts index 1cb0d6b..634bde4 100644 --- a/test/Command.test.ts +++ b/test/Command.test.ts @@ -5,19 +5,19 @@ import * as Command from "@effect/cli/Command" import * as CommandDirective from "@effect/cli/CommandDirective" import * as HelpDoc from "@effect/cli/HelpDoc" import * as Options from "@effect/cli/Options" -import * as Terminal from "@effect/cli/Terminal" import * as Grep from "@effect/cli/test/utils/grep" import * as Tail from "@effect/cli/test/utils/tail" import * as WordCount from "@effect/cli/test/utils/wc" import * as ValidationError from "@effect/cli/ValidationError" import * as FileSystem from "@effect/platform-node/FileSystem" +import * as Terminal from "@effect/platform-node/Terminal" import * as Doc from "@effect/printer/Doc" import * as Render from "@effect/printer/Render" import { Effect, Option, ReadonlyArray, String } from "effect" import * as Layer from "effect/Layer" import { describe, expect, it } from "vitest" -const MainLive = Layer.merge(FileSystem.layer, Terminal.LiveTerminal) +const MainLive = Layer.merge(FileSystem.layer, Terminal.layer) const runEffect = ( self: Effect.Effect @@ -269,7 +269,7 @@ describe("Command", () => { HelpDoc.p("this is some help") ) expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expectedValue)) - expect(cmd.help).toEqual(expectedDoc) + expect(cmd.help()).toEqual(expectedDoc) }).pipe(runEffect)) it("should allow adding help documentation to subcommands", () => { @@ -277,7 +277,7 @@ describe("Command", () => { Command.standard("sub").pipe(Command.withHelp("this is some help")) ])) const expected = HelpDoc.sequence(HelpDoc.h1("DESCRIPTION"), HelpDoc.p("this is some help")) - expect(cmd.help).not.toEqual(expected) + expect(cmd.help()).not.toEqual(expected) }) it("should correctly display help documentation for a command", () => { @@ -291,7 +291,7 @@ describe("Command", () => { Command.withHelp("help 1") ) const parent = Command.standard("parent").pipe(Command.subcommands([child1])) - const result = Render.prettyDefault(Doc.unAnnotate(HelpDoc.toAnsiDoc(parent.help))) + const result = Render.prettyDefault(Doc.unAnnotate(HelpDoc.toAnsiDoc(parent.help()))) expect(result).toBe(String.stripMargin( `|COMMANDS | diff --git a/test/Completion.test.ts b/test/Completion.test.ts index 9f5a97f..4c1fd67 100644 --- a/test/Completion.test.ts +++ b/test/Completion.test.ts @@ -15,7 +15,7 @@ import * as Order from "effect/Order" import * as ReadonlyArray from "effect/ReadonlyArray" import { describe, expect, it } from "vitest" -const MainLive = Layer.provideMerge(NodeContext.layer, Compgen.LiveCompgen) +const MainLive = Layer.provideMerge(NodeContext.layer, Compgen.layer) const runEffect = ( self: Effect.Effect @@ -254,7 +254,7 @@ describe("Completion", () => { "fooDir/" ] expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -284,7 +284,7 @@ describe("Completion", () => { ) const expected = ["foo.txt ", "fooDir/"] expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -314,7 +314,7 @@ describe("Completion", () => { ) const expected = ["foo.txt "] expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -344,7 +344,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.empty() expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -374,7 +374,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.make("barDir/", "bippyDir/", "fooDir/") expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -404,7 +404,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.make("fooDir/") expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -434,7 +434,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.make("fooDir/") expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) }) @@ -842,7 +842,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.make("foo.txt ", "fooDir/") expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) }) @@ -888,7 +888,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.make("foo.txt ", "fooDir/") expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) @@ -918,7 +918,7 @@ describe("Completion", () => { ) const expected = ReadonlyArray.make("foo.txt ", "fooDir/") expect(result).toEqual(expected) - }).pipe(Effect.provide(Compgen.TestCompgen(tempDir))) + }).pipe(Effect.provide(Compgen.testLayer(tempDir))) ) }).pipe(Effect.scoped, runEffect)) }) diff --git a/test/Primitive.test.ts b/test/Primitive.test.ts index 02564c6..6e21a87 100644 --- a/test/Primitive.test.ts +++ b/test/Primitive.test.ts @@ -118,7 +118,7 @@ const simplePrimitiveTestSuite = ( const result = yield* _( Effect.flip(primitive.validate(Option.some("bad"), CliConfig.defaultConfig)) ) - expect(result).toBe(`'bad' is not a ${primitive.typeName}`) + expect(result).toBe(`'bad' is not a ${primitive.typeName()}`) }).pipe(runEffect)) }) }