diff --git a/README.md b/README.md index 0bd6d13..37d17f2 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,127 @@ # @reliverse/prompts -[**Docs**](.github/DOCS.md) | [**npmjs.com**](https://npmjs.com/package/@reliverse/prompts) | [**GitHub**](https://github.com/reliverse/prompts) | [**Discord**](https://discord.gg/Pb8uKbwpsJ) - -

- - version - - - - downloads - - - -

- -@reliverse/prompts is a powerful library that enables seamless, type-safe, and resilient prompts for command-line applications. Crafted with simplicity and elegance, it provides developers with an intuitive and robust way to build interactive CLIs. - -@reliverse/prompts is a full-featured alternative to @inquirer/prompts (Inquirer.js), enquirer, @clack/prompts, terkelg prompts, cronvel terminal-kit, unjs consola, and similar libraries. - -## Installation - -Install with your preferred package manager: - -```sh -bun add @reliverse/prompts # Replace 'bun' with npm, pnpm, or yarn if desired (deno and jsr support coming soon) +> **The CLI prompt library you didn’t realize you’ve been desperate for.** + +It’s blazing-fast, type-safe, and has built-in crash resilience—so your command-line app can look slick without bursting into flames. Forget boilerplate-heavy setups—this library makes CLI development smooth and effortless. + +## Rapid-Fire Overview + +
+ version + downloads +
+ +- **Docs**: [docs.reliverse.org/reliverse/prompts](https://docs.reliverse.org/reliverse/prompts/) +- **NPM**: [npmjs.com/package/@reliverse/prompts](https://www.npmjs.com/package/@reliverse/prompts) +- **GitHub**: [github.com/reliverse/prompts](https://github.com/reliverse/prompts) +- **Discord**: [discord.gg/3GawfWfAPe](https://discord.gg/3GawfWfAPe) + +## Install in 3.2 Seconds + +```bash +bun add @reliverse/prompts +# Or npm/pnpm/yarn if that’s your style +# (deno with jsr support soon™) ``` -## Screenshot +> **Pro Tip**: Make sure you have [Bun](https://bun.sh), [Node.js](https://nodejs.org), and [Git](https://git-scm.com/downloads) installed. + +## The 3-Second Pitch + +> This thing is **blazing fast**, **type-safe**, and basically a bulletproof vest for your CLI. +> No more spaghetti prompt code. Just shiny, stable, next-gen developer bliss. + +### *“But why not stick with Inquirer or Clack?”* + +1. Better typed validation, bigger ASCII art, more interactive goodies, and next-level color theming. +2. Because you deserve better than “just good enough.” -[![CLI Example](./example.png)](./example.png) +## Key Selling Points -## Key Features +- **TypeScript-first**: So your TypeScript dev heart can flutter in peace. Enjoy IntelliSense and zero guesswork. +- **Flexible Prompt Types**: Text, confirm, select, multiselect, password, number, spinner, toggle, and many more… you do you. +- **Schema-driven Validations**: Easily integrates with TypeBox, Zod, or your own thing. No more “hope it works” solutions. +- **Accessibility**: Terminal-size awareness, WCAG AA color contrast, the whole enchilada. +- **Crash-safe**: Ctrl+C or random cosmic rays? It shrugs them off. -- **Type Safety**: Built with TypeScript, ensuring strong typing to prevent runtime errors. -- **Schema Validation**: Validates user inputs using schemas for enhanced reliability. -- **Flexible Prompt Types**: Supports a range of prompt types, including text, password, number, select, and multiselect. -- **Crash Resilience**: Designed to handle cancellations and errors gracefully, ensuring stability. +## Why @reliverse/prompts? -## Prompt Types +Consider it your brand-new sports car, while your old CLI prompt library was a rusty lawnmower. Also, it’s a feature-packed replacement for Inquirer, Enquirer, Clack, Terkelg, Terminal-Kit, and a bunch more. -Each type has its own validation and display logic. More types are planned for future releases. +[**Go deeper in the docs →**](https://docs.reliverse.org/reliverse/prompts/) -- **Text**: Collects text input. -- **Password**: Hidden input for secure password entries. -- **Number**: Numeric input with optional validation. -- **Confirm**: Simple Yes/No prompt. -- **Select**: Dropdown selection for multiple choices. -- **Multiselect**: Allows users to select multiple items from a list. +### Straight-Up GOAT Features -## Input Validation +- **Full Cross-Platform ESM**: Seamlessly works with the Node.js and Bun environments. +- **Extensible UI**: Because color, typography, animations, and more matter to your terminal fashion sense. +- **Built for DX**: Minimal dependencies, clean API, full validation baked in, and more. +- **Mono Component**: Perfect for rapid prototyping. Or if you’re feeling lazy. -All prompts support custom validation logic, providing immediate feedback to users. +## Speedrun Example + +```ts +import { inputPrompt } from "@reliverse/prompts"; + +await startPrompt({ + clearConsole: true, + titleColor: "inverse", + packageName: "@reliverse/cli", + packageVersion: "1.0.0", + terminalSizeOptions: { + minWidth: 100, + minHeight: 16, + // 🗴 Oops! Terminal width is too small. Expected >100 | Current: 97 + }, +}); + +const username = await inputPrompt({ + id: "username", + title: "Welcome to @reliverse/prompts Demo!", + content: "What's your name?", +}); + +console.log(`Hey there, ${username}!`); +``` -## Contributing +## Task Management -@reliverse/prompts is a work in progress. We welcome feedback and contributions to help make it the best library it can be. Thank you! +The library provides a powerful task management system with built-in verification steps, spinners, and error handling: -Here is how to install the library for development: +- **Spinner and progress bars that actually move**: Progress tracking with current/total counts and status messages +- **Task priorities (because some stuff is more important)**: Critical, high, normal, low +- **Built-in stats, error handling, and cancellation**: Task timing and statistics, built-in error handling and cancellation support +- **Customizable spinners to keep your eyes happy**: Customizable spinners and progress indicators +- **Nested subtasks and task groups**: Group tasks and subtasks for better organization +- **Automatic verification steps with customizable delays** +- **Simple error handling with proper formatting** +- **Progress tracking for long-running operations** +- **Custom validation and business logic support** -```sh +Visit [docs](https://docs.reliverse.org/reliverse/prompts/#task-management) to learn more and see examples. + +## Playground Mode + +```bash git clone https://github.com/reliverse/prompts.git cd prompts bun i +bun dev ``` -## Playground +Check out `examples/launcher.ts` for a smorgasbord of demos (including a quiz). Who says CLIs can’t be fun? -Run `bun dev` to launch the [examples/launcher.ts](./examples/launcher.ts) CLI, which helps you to dive into and explore any of the examples listed below. Experiment with @reliverse/prompts by running examples locally or reviewing the linked code: +## Examples to Copy-Paste -1. **[1-main.ts](./examples/1-main.ts)**: A comprehensive example of a CLI application featuring a well styled UI config. This example showcases all available prompt components, with code organized into separate functions and files for better readability and clarity. -2. **[2-mono.ts](./examples/2-mono.ts)**: A quiz game example inspired by Fireship's video about CLIs. It demonstrates the dynamic capabilities of @reliverse/prompts by using a prompt() that includes all prompt components, so you don't need to import each component separately. -3. **[3-simple.ts](./examples/3-simple.ts)**: A simple example highlighting the core functionalities of @reliverse/prompts. The entire implementation is contained within a single file for easy understanding. -4. **[4-args-a.ts](./examples/4-args-a.ts)**: An example of how to run commands using arguments. -5. **[5-args-b.ts](./examples/5-args-b.ts)**: Another example of how to run commands using arguments. +1. **1-main.ts**: A powerhouse CLI example with all the trimmings, with advanced styling and all prompts. +2. **2-mono.ts**: A single `prompt()` for multiple components—perfect for CLI where performance doesn't matter. +3. **3-simple.ts**: Less code, more speed. +4. **4-args-a.ts** + **5-args-b.ts**: Turn sub-commands into a more headless experience. -## Extendable Configuration +## Custom Config FTW -**Example Configuration:** +You don’t want a one-size-fits-all library. We got you: -```typescript +```ts const basicConfig = { titleColor: "cyanBright", titleTypography: "bold", @@ -101,110 +136,60 @@ const extendedConfig = { const username = await inputPrompt({ id: "username", - title: "We're glad you're testing our library!", - content: "Let's get to know each other!\nWhat's your username?", - schema: schema.properties.username, + title: "Testing out our fancy library!", + content: "What's your username?", ...extendedConfig, }); ``` -## Mono Component +## Mono Component: One Import to Rule Them All -The Mono Component is a special component that includes all other components. It's a great way to get started quickly or to see how all the components work together. - -This component requires providing prompt id. To have typesafety use something like the following: +If you’re lazy (like the rest of us), in a hurry, or just want everything jammed together, the Mono Component wraps up all prompt types in a single import. ```ts export const IDs = { start: "start", username: "username", - dir: "dir", - spinner: "spinner", - password: "password", - age: "age", - lang: "lang", - color: "color", - birthday: "birthday", - features: "features", + // ... }; ``` -## Prompts Library Comparison - -> **Note:** This table contains approximate and placeholder values. More detailed assessments will be provided as libraries continue to evolve. - -The mission of @reliverse/prompts is to achieve the 🟢 in all categories. - -**Icon Legend:** - -- 🟡: Not yet verified -- 🟢: Fully supported -- 🔵: Partially supported -- 🔴: Not supported - -| **Feature** | **@reliverse/prompts** | **@inquirer/prompts** | **@enquirer/enquirer** | **@clack/prompts** | **@terkelg/prompts** | **@cronvel/terminal-kit** | **@unjs/citty** | **@unjs/consola** | **@chjj/blessed** | -| -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------ | ---------------------- | ------------------ | -------------------- | ------------------------- | --------------- | ------------------ | ----------------- | -| - Full ES Modules Support | 🟢 Native ES module package | 🟢 | 🟡 | 🟡 | 🔴 CJS-only | 🔴 CJS-only | 🟢 | 🟢 | 🟡 | -| - Works both in node, bun, and deno environments | 🔵 node+bun (deno support is coming soon) | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🔵 | 🔵 | 🟡 | -| - Codebase typesafety with intellisense | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Runtime typesafety with schema validation | 🟢 TypeBox & Custom | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Usage Examples | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Customization | 🟢 Colors, typography, variants, borders, and more | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Custom Validation | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Error Handling | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Ease of Setup | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Crash Resilience | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - General DX | 🔵 Clean and understandable TypeScript code | 🟡 | 🟡 | 🟡 | 🔴 JS-only | 🔴 JS-only | 🟡 | 🟡 | 🟡 | -| - DX: Classes | 🟢 Minimal number of classes as possible | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Documentation | 🔵 | 🟡 | 🟡 | 🟡 | 🔵 | 🟢 | 🟡 | 🟡 | 🟢 | -| - Extendable APIs | 🟢 Works well with @reliverse/relinka | 🟢 Huge number of community prompts | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Designed With UX/DX in Mind | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - All components support Ctrl+C | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - As minimal number of dependencies as possible | 🔵 | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------ | ---------------------- | ------------------ | -------------------- | ------------------------- | --------------- | ------------------ | ----------------- | -| **Components** | | | | | | | | | | -| -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------ | ---------------------- | ------------------ | -------------------- | ------------------------- | --------------- | ------------------ | ----------------- | -| - Visual Components | 🟢 Animated Text (incl. 6 anims) & ASCII Art (incl. 290 fonts) | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Mono Component | 🟢 Mono (All-In-One) & Separate | 🟡 | 🟡 | 🟡 | 🔵 Mono-only | 🟡 | 🟡 | 🟡 | 🟡 | -| - Start Component | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Text Component | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Password Component | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Number Component | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Confirm Component | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Select Component | 🔵 Native separator support | 🔵 Separator supported via `new` keyword | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Multiselect Component | 🔵 Native separator support | 🔵 Separator supported via `new` keyword | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Search/Autocomplete Component | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | -| - Task/Spinner & Progressbar Components | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | 🟡 | -| - Image Component | 🔴 Planned | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | 🟡 | -| - Range Component | 🔵 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | 🟡 | -| -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------ | ---------------------- | ------------------ | -------------------- | ------------------------- | --------------- | ------------------ | ----------------- | -| **Arguments Support** | | | | | | | | | | -| -------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------ | ---------------------- | ------------------ | -------------------- | ------------------------- | --------------- | ------------------ | ----------------- | -| - Fast and lightweight argument parser | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | -| - Smart value parsing with typecast | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | -| - Boolean shortcuts and unknown flag handling | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | -| - Nested sub-commands | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | -| - Lazy and Async commands | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | -| - Pluggable and composable API | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | -| - Auto generated usage and help | 🟢 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟢 | 🟡 | 🟡 | - -**Related Links**: [@reliverse/relinka](https://github.com/reliverse/relinka#readme), [ESM/CJS](https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm), ["Pure ESM package"](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c), [Clean code](https://github.com/ryanmcdermott/clean-code-javascript#readme), ["UX patterns for CLI tools"](https://lucasfcosta.com/2022/06/01/ux-patterns-cli-tools.html), [DX (Developer Experience)](https://github.blog/enterprise-software/collaboration/developer-experience-what-is-it-and-why-should-you-care), [TypeBox](https://github.com/sinclairzx81/typebox#readme), ["ANSI Escape Sequences"](https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b), [@chjj/blessed](https://github.com/chjj/blessed#readme), [Wrapping](https://github.com/SBoudrias/Inquirer.js/pull/255), [Visual Control](https://stackoverflow.com/questions/68344362/how-do-i-get-full-visual-control-of-a-terminal-with-node) - -## Wrap-Up - -@reliverse/prompts is a versatile library designed to accelerate CLI development by providing customizable prompt components. Integrated into the [Reliverse CLI](https://github.com/blefnk/reliverse#readme), @reliverse/prompts enables you to create a unique design aligned with your CLI app’s aesthetics, similar to how @shadcn/ui supports customizable web UI components. Quickly get started by copying configurations from the [Reliverse Docs](https://docs.reliverse.org/prompts) and using components that fit your project, making it faster to bring your CLI app to life. You’re free to customize each component as desired, with default designs provided to ensure an attractive interface from the start. - -## Learn More - -- [Temporary @reliverse/prompts Docs](.github/DOCS.md) -- [Reliverse Docs](https://docs.reliverse.org) +## Comparison Table -## Special Thanks +We’re not shy. We lined up **@reliverse/prompts** against Inquirer, Clack, Terminal-Kit, etc. Our goal? **Turn every feature dot green**. [**Check out the epic chart**](https://docs.reliverse.org/reliverse/prompts/#prompts-library-comparison) + +## Arguments Support + +You can’t build an amazing CLI without argument parsing. We’ve got a built-in fast parser that typecasts your things automatically. + +## Wrap It Up + +@reliverse/prompts is more than just pretty and fast prompts—it’s a full-blown CLI builder with customizable designs and robust typing. It’s built to slot seamlessly into Reliverse’s ecosystem, but even if you’re rolling your own thing, you’ll appreciate the minimal boilerplate and fancy visuals. -This project wouldn’t exist without the amazing work of the huge number of contributors to the list of projects below. Many of the @reliverse/prompts prompts are based on the incredible work of: +- **CLI builder** with style & resilience +- **Customizable** design and color theming +- **Zero guesswork** with TypeScript integrations +- **Minimal boilerplate** with maximum results -[@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js#readme) | [terkelg/prompts](https://github.com/terkelg/prompts#readme) | [@clack/prompts](https://github.com/bombshell-dev/clack#readme) | [create-t3-app](https://github.com/t3-oss/create-t3-app#readme) | [create-astro](https://github.com/withastro/astro/tree/main/packages/create-astro#readme) | [cronvel/terminal-kit](https://github.com/cronvel/terminal-kit#readme) | [unjs/consola](https://github.com/unjs/consola#readme) | [nodejs/string_decoder](https://github.com/nodejs/string_decoder) | [TooTallNate/keypress](https://github.com/TooTallNate/keypress) | [derhuerst](https://github.com/derhuerst) +## Special Thanks + +**@inquirer/prompts**, **@terkelg/prompts**, **@clack/prompts**, **@unjs/citty**, and all the open-source legends. You built the shoulders we stand on. ## License -[MIT](./LICENSE.md) © [Nazarii Korniienko](https://github.com/blefnk) +**MIT** © [Nazarii Korniienko](https://github.com/reliverse/prompts) + +> **Stop reading. Start coding.** +> If you’re serious about CLIs—don’t just build—**Reliverse it** with `@reliverse/prompts`. + +## More Goodies + +- [**Reliverse Docs**](https://docs.reliverse.org/reliverse/prompts/) + +## Screenshot Brag + +[![CLI Example](./examples/1.png)](./examples/1.png) + +**That’s it.** Now go forth and build unstoppable CLIs with `@reliverse/prompts`. And remember: + +> **Don’t just build a CLI. Reliverse it.** diff --git a/bun.lockb b/bun.lockb index e81d639..4d0b7f1 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cspell.json b/cspell.json index 22578bc..70b316e 100644 --- a/cspell.json +++ b/cspell.json @@ -5,6 +5,7 @@ "ignorePaths": ["examples/deprecated", "dist-jsr", "dist-npm"], "words": [ "alacritty", + "alcalzone", "alternar", "anims", "anykey", @@ -18,6 +19,7 @@ "ausgewählt", "avez", "Baratheon", + "bitflag", "blefnk", "browserlist", "browserlistrc", @@ -54,13 +56,16 @@ "figliolia", "Fireship", "Focusable", + "focusables", "forgetme", + "Gawf", "Geralt", "goroutines", "Greyjoy", "Gyllenhaal", "haben", "Haha", + "hotmail", "hoverable", "iife", "invertir", @@ -105,15 +110,19 @@ "sbuf", "scelto", "scule", + "segs", "selección", "seleccionar", "sentencer", "shadcn", + "shoutout", "signup", "Silverhand", "sisteransi", + "Speedrun", "Sprache", "subchoices", + "suped", "Targ", "Targaryen", "terkelg", @@ -126,13 +135,16 @@ "typesafety", "typestat", "Tyrell", + "unclip", "unjs", "unpub", "unstub", "valign", + "valtio", "venv", "Vous", "vsprintf", + "WCAG", "wezterm", "Whoo", "Wybrałeś", diff --git a/example.png b/example.png deleted file mode 100644 index e29d724..0000000 Binary files a/example.png and /dev/null differ diff --git a/examples/launcher.ts b/examples/launcher.ts index 6341251..2659ed1 100644 --- a/examples/launcher.ts +++ b/examples/launcher.ts @@ -9,64 +9,81 @@ async function examplesRunner() { const exampleToRun = await selectPrompt({ title: "Choose an example to run", + titleColor: "passionGradient", options: [ { - label: "✨ The Most Full-Featured Example", - value: "1-main", + label: "✨ Full-Featured Example", + value: "main", hint: "recommended", }, { - label: pc.dim("Mono Component Example"), - value: "2-mono", + label: "Spinner Example", + value: "spinner", + hint: "experimental", + }, + { + label: pc.dim("Task Example"), + value: "task", + hint: pc.dim("not finished"), + }, + { + label: pc.dim("Progressbar Example"), + value: "progressbar", hint: pc.dim("not finished"), }, { label: pc.dim("Simple Example"), - value: "3-simple", + value: "simple", hint: pc.dim("not finished"), }, { label: pc.dim("with flags 1 Example"), - value: "4-cmd-a", + value: "cmd-a", hint: pc.dim("not finished"), }, { label: pc.dim("with flags 2 Example"), - value: "5-cmd-b", + value: "cmd-b", hint: pc.dim("not finished"), }, { label: "🗝️ Exit", value: "exit" }, ] as const, - defaultValue: "1-main", + defaultValue: "main", }); switch (exampleToRun) { - case "1-main": - await import("./1-main.js"); + case "main": + await import("./main.js"); + break; + case "spinner": + await import("./other/spinner.js"); + break; + case "task": + await import("./other/task.js"); break; - case "2-mono": - await import("./2-mono.js"); + case "progressbar": + await import("./other/progress.js"); break; - case "3-simple": - await import("./3-simple.js"); + case "simple": + await import("./other/simple.js"); break; - case "4-cmd-a": + case "cmd-a": console.clear(); console.log( - "`bun examples/4-args-a.ts Alice --friendly --age 22 --adj cool`", + "`bun examples/other/args-a.ts Alice --friendly --age 22 --adj cool`", ); console.log("Run without any arguments to see the help message."); break; - case "5-cmd-b": + case "cmd-b": console.clear(); console.log( - "1. [BUILD] `bun examples/5-args-b.ts build ./src --workDir ./src`", + "1. [BUILD] `bun examples/other/args-b.ts build ./src --workDir ./src`", ); console.log( - "2. [DEBUG] `bun examples/5-args-b.ts debug --feature database-query`", + "2. [DEBUG] `bun examples/other/args-b.ts debug --feature database-query`", ); console.log( - "3. [DEPLOY] `bun examples/5-args-b.ts deploy --include '*.js' --exclude '*.d.ts'`", + "3. [DEPLOY] `bun examples/other/args-b.ts deploy --include '*.js' --exclude '*.d.ts'`", ); console.log("Run without any arguments to see the help message."); break; diff --git a/examples/main.png b/examples/main.png new file mode 100644 index 0000000..5fd48e5 Binary files /dev/null and b/examples/main.png differ diff --git a/examples/1-main.ts b/examples/main.ts similarity index 100% rename from examples/1-main.ts rename to examples/main.ts diff --git a/examples/4-args-a.ts b/examples/other/args-a.ts similarity index 100% rename from examples/4-args-a.ts rename to examples/other/args-a.ts diff --git a/examples/5-args-b.ts b/examples/other/args-b.ts similarity index 54% rename from examples/5-args-b.ts rename to examples/other/args-b.ts index b2d33aa..e8321c3 100644 --- a/examples/5-args-b.ts +++ b/examples/other/args-b.ts @@ -1,6 +1,6 @@ import { defineCommand, runMain } from "~/main.js"; -import packageJson from "../package.json" with { type: "json" }; +import packageJson from "../../package.json" with { type: "json" }; const main = defineCommand({ meta: { @@ -15,9 +15,9 @@ const main = defineCommand({ console.info("✅ Cleanup"); }, subCommands: { - build: () => import("./src/commands/build.js").then((r) => r.default), - deploy: () => import("./src/commands/deploy.js").then((r) => r.default), - debug: () => import("./src/commands/debug.js").then((r) => r.default), + build: () => import("../src/commands/build.js").then((r) => r.default), + deploy: () => import("../src/commands/deploy.js").then((r) => r.default), + debug: () => import("../src/commands/debug.js").then((r) => r.default), }, }); diff --git a/examples/2-mono.ts b/examples/other/progress.ts similarity index 62% rename from examples/2-mono.ts rename to examples/other/progress.ts index 19687ea..ccc53c3 100644 --- a/examples/2-mono.ts +++ b/examples/other/progress.ts @@ -1,6 +1,12 @@ // 2-mono-example.ts: A fun example of a quiz game. Inspired by CLI-game created by Fireship. The example demonstrates how to use a mono prompt() component. -import { animateText, inputPrompt, task } from "~/main.js"; +import { + animateText, + inputPrompt, + advancedTaskPrompt, + endPrompt, + msg, +} from "~/main.js"; import { prompt } from "~/mono/mono.js"; import { colorize } from "~/utils/colorize.js"; import { errorHandler } from "~/utils/errors.js"; @@ -48,7 +54,7 @@ async function main() { ], }); - await task({ + /* await advancedTaskPrompt({ initialMessage: "Checking answer...", successMessage: "Answer checked successfully.", spinnerSolution: "ora", @@ -58,11 +64,54 @@ async function main() { updateMessage( answer === "Dec 4th, 1995" ? `Nice work ${playerName}. That's a legit answer!` - : `🫠 Game over, ${playerName}! You lose!`, + : `🫠 Game over, ${playerName}! You lose!`, ); await new Promise((resolve) => setTimeout(resolve, 1000)); }, - }); + }); */ + + const result = await advancedTaskPrompt( + "Check answer", + { + priority: "normal", + displayType: "progress", + }, + async ({ setStatus, setError, setProgress }) => { + setProgress({ + current: 0, + total: 5, + message: "Starting verification...", + }); + await new Promise((resolve) => setTimeout(resolve, 500)); + + for (let i = 0; i < 5; i++) { + setProgress({ + current: i + 1, + total: 5, + message: `Step ${i + 1}: Verifying answer...`, + }); + await new Promise((resolve) => setTimeout(resolve, 400)); + } + + const isCorrect = answer === "Dec 4th, 1995"; + + if (!isCorrect) { + setError( + `🫠 Game over, ${playerName}! You lose!\nNo worries, you can try again!`, + ); + // Wait a bit to ensure the message is displayed + await new Promise((resolve) => setTimeout(resolve, 100)); + process.exit(1); + } + + setStatus(`Nice work ${playerName}. That's a legit answer!`); + return isCorrect; + }, + ); + + if (!result) { + return; + } const message = `Congrats !\n $ 1 , 0 0 0 , 0 0 0`; @@ -81,6 +130,15 @@ async function main() { titleTypography: "bold", border: false, }); + + msg({ type: "M_BAR" }); + + await endPrompt({ + title: "Thanks for playing!\n", + titleColor: "passionGradient", + titleTypography: "bold", + }); + process.exit(0); } diff --git a/examples/separate/select/select-example.ts b/examples/other/select.ts similarity index 66% rename from examples/separate/select/select-example.ts rename to examples/other/select.ts index 7a966a2..d85c4f2 100644 --- a/examples/separate/select/select-example.ts +++ b/examples/other/select.ts @@ -17,39 +17,39 @@ export async function detailedExample() { msg({ type: "M_INFO", title: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", content: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", - hint: "hint !!! What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", + hint: "hint !!! What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", }); await confirmPrompt({ title: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", content: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", displayInstructions: true, }); await confirmPrompt({ title: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", content: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", }); await togglePrompt({ title: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", content: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", }); await selectPrompt({ title: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", content: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", displayInstructions: true, terminalWidth: 92, options: [ @@ -68,9 +68,9 @@ export async function detailedExample() { await multiselectPrompt({ title: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", content: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", defaultValue: ["react", "typescript"], options: [ { @@ -93,9 +93,9 @@ export async function detailedExample() { await selectPrompt({ title: - "What web technologies do you like? 42 is the answer to everything. What do you think about testing the very long text? Is something broken for you? What category best describes your project?", + "What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very long text? Is something broken for you? What category best describes your project?", content: - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?", + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?", defaultValue: "react", options: [ { diff --git a/examples/3-simple.ts b/examples/other/simple.ts similarity index 81% rename from examples/3-simple.ts rename to examples/other/simple.ts index 04529dd..2b334bc 100644 --- a/examples/3-simple.ts +++ b/examples/other/simple.ts @@ -5,18 +5,18 @@ import pc from "picocolors"; import figures from "~/figures/index.js"; import { select } from "~/prompts/index.js"; -import checkboxDemo from "./src/simple/checkbox.js"; -import confirmDemo from "./src/simple/confirm.js"; -import editorDemo from "./src/simple/editor.js"; -import expandDemo from "./src/simple/expand.js"; -import inputDemo from "./src/simple/input.js"; -import loaderDemo from "./src/simple/loader.js"; -import numberDemo from "./src/simple/number.js"; -import passwordDemo from "./src/simple/password.js"; -import rawlistDemo from "./src/simple/rawlist.js"; -import searchDemo from "./src/simple/search.js"; -import selectDemo from "./src/simple/select.js"; -import timeoutDemo from "./src/simple/timeout.js"; +import checkboxDemo from "../src/simple/checkbox.js"; +import confirmDemo from "../src/simple/confirm.js"; +import editorDemo from "../src/simple/editor.js"; +import expandDemo from "../src/simple/expand.js"; +import inputDemo from "../src/simple/input.js"; +import loaderDemo from "../src/simple/loader.js"; +import numberDemo from "../src/simple/number.js"; +import passwordDemo from "../src/simple/password.js"; +import rawlistDemo from "../src/simple/rawlist.js"; +import searchDemo from "../src/simple/search.js"; +import selectDemo from "../src/simple/select.js"; +import timeoutDemo from "../src/simple/timeout.js"; const demos = { checkbox: checkboxDemo, diff --git a/examples/other/spinner.ts b/examples/other/spinner.ts new file mode 100644 index 0000000..7e62d13 --- /dev/null +++ b/examples/other/spinner.ts @@ -0,0 +1,126 @@ +import { + endPrompt, + inputPrompt, + msg, + selectPrompt, + startPrompt, +} from "~/main.js"; +import { prompt } from "~/mono/mono.js"; +import { spinnerTaskPrompt } from "~/task/spinner.js"; +import { colorize } from "~/utils/colorize.js"; +import { errorHandler } from "~/utils/errors.js"; +import { createAsciiArt } from "~/visual/ascii-art/ascii-art.js"; + +async function main() { + console.clear(); + await startPrompt({ + title: "Spinner Component Example", + titleColor: "passionGradient", + titleTypography: "bold", + }); + + msg({ + type: "M_GENERAL", + title: "Who Wants to Be a JS Mil?", + titleColor: "gradientGradient", + }); + + msg({ + type: "M_GENERAL", + symbol: "middle", + title: ` + ${colorize("HOW TO PLAY", "white", "bold")} + I am a process on your computer. + If you get any question wrong I will be ${colorize("killed", "red", "bold")} + So get all the questions right... + `, + }); + + const player_name = await inputPrompt({ + title: "What is your name?", + defaultValue: "Player", + }); + const playerName = player_name ?? "Player"; + + const answer = await selectPrompt({ + title: "JavaScript was created in 10 days then released on", + options: [ + { label: "May 23rd, 1995", value: "May 23rd, 1995" }, + { label: "Nov 24th, 1995", value: "Nov 24th, 1995" }, + { label: "Dec 4th, 1995", value: "Dec 4th, 1995" }, + { label: "Dec 17, 1996", value: "Dec 17, 1996" }, + ], + }); + + await spinnerTaskPrompt({ + initialMessage: "Checking your answer...", + successMessage: `Nice work ${playerName}. That's a legit answer!`, + errorMessage: `🫠 Game over, ${playerName}! You lose!`, + delay: 100, + spinnerSolution: "ora", + spinnerType: "arc", + action: async (updateMessage) => { + const isCorrect = answer === "Dec 4th, 1995"; + if (!isCorrect) { + updateMessage(`🫠 Game over, ${playerName}! You lose!`); + process.exit(1); + } + }, + }); + + const companyAnswer = await selectPrompt({ + title: "Which company created JavaScript?", + options: [ + { label: "Microsoft", value: "Microsoft" }, + { label: "Netscape", value: "Netscape" }, + { label: "Mozilla", value: "Mozilla" }, + { label: "Google", value: "Google" }, + ], + }); + + await spinnerTaskPrompt({ + initialMessage: "Which company created JavaScript?", + successMessage: "Correct! Netscape created JavaScript.", + errorMessage: "Wrong! Netscape created JavaScript.", + delay: 100, + spinnerSolution: "ora", + spinnerType: "arc", + action: async (updateMessage) => { + const isCorrect = companyAnswer === "Netscape"; + if (!isCorrect) { + updateMessage("Wrong answer!"); + process.exit(1); + } + }, + }); + + const message = `Congrats !\n $ 2 , 0 0 0 , 0 0 0`; + + await createAsciiArt({ + message, + font: "Standard", + }); + + await prompt({ + type: "end", + id: "winner", + title: ` + Programming isn't about what you know; + it's about making the command line look cool!`, + titleColor: "bgCyanBright", + titleTypography: "bold", + border: false, + }); + + msg({ type: "M_BAR" }); + + await endPrompt({ + title: "Thanks for playing!\n", + titleColor: "passionGradient", + titleTypography: "bold", + }); + + process.exit(0); +} + +await main().catch((error) => errorHandler(error)); diff --git a/examples/other/task.ts b/examples/other/task.ts new file mode 100644 index 0000000..8b1c7ae --- /dev/null +++ b/examples/other/task.ts @@ -0,0 +1,155 @@ +import { + inputPrompt, + msg, + selectPrompt, + startPrompt, + advancedTaskPrompt, + endPrompt, +} from "~/main.js"; +import { prompt } from "~/mono/mono.js"; +import { colorize } from "~/utils/colorize.js"; +import { errorHandler } from "~/utils/errors.js"; +import { createAsciiArt } from "~/visual/ascii-art/ascii-art.js"; + +async function main() { + console.clear(); + await startPrompt({ + title: "Task Component Example", + titleColor: "passionGradient", + titleTypography: "bold", + }); + + msg({ + type: "M_GENERAL", + title: "Who Wants to Be a JS Mil?", + titleColor: "gradientGradient", + }); + + msg({ + type: "M_GENERAL", + symbol: "middle", + title: ` + ${colorize("HOW TO PLAY", "white", "bold")} + I am a process on your computer. + If you get any question wrong I will be ${colorize("killed", "red", "bold")} + So get all the questions right... + `, + }); + + const player_name = await inputPrompt({ + title: "What is your name?", + defaultValue: "Player", + }); + const playerName = player_name ?? "Player"; + + const answer = await selectPrompt({ + title: "JavaScript was created in 10 days then released on", + options: [ + { label: "May 23rd, 1995", value: "May 23rd, 1995" }, + { label: "Nov 24th, 1995", value: "Nov 24th, 1995" }, + { label: "Dec 4th, 1995", value: "Dec 4th, 1995" }, + { label: "Dec 17, 1996", value: "Dec 17, 1996" }, + ], + }); + + const firstResult = await advancedTaskPrompt.group( + { priority: "normal" }, + (create) => [ + create( + "Check answer", + { + priority: "normal", + displayType: "spinner", + spinnerType: "arc", + exitProcessOnError: true, + }, + async ({ setError }) => { + const isCorrect = answer === "Dec 4th, 1995"; + + if (!isCorrect) { + setError(new Error(`🫠 Game over, ${playerName}! You lose!`)); + return false; + } + + return `Nice work ${playerName}. That's a legit answer!`; + }, + ), + ], + ); + + if (firstResult[0].result) { + const answer2 = await selectPrompt({ + title: "Which company created JavaScript?", + options: [ + { label: "Microsoft", value: "Microsoft" }, + { label: "Netscape", value: "Netscape" }, + { label: "Oracle", value: "Oracle" }, + { label: "Google", value: "Google" }, + ], + }); + + const secondResult = await advancedTaskPrompt.group( + { priority: "normal" }, + (create) => [ + create( + "Validate answer", + { + priority: "normal", + displayType: "spinner", + spinnerType: "arc", + }, + async ({ setError }) => { + const isCorrect = answer2 === "Netscape"; + if (!isCorrect) { + setError( + new Error(`Wrong! Netscape created JavaScript in 1995.`), + ); + return false; + } + return "Correct! Netscape created JavaScript."; + }, + ), + ], + ); + + if (secondResult[0].result === false) { + msg({ + type: "M_GENERAL", + title: "Game Over! You got the first one but missed the second.", + }); + process.exit(1); + } + + const message = `Congrats !\n $ 2 , 0 0 0 , 0 0 0`; + + await createAsciiArt({ + message, + font: "Standard", + }); + + await prompt({ + type: "end", + id: "winner", + title: ` + Programming isn't about what you know; + it's about making the command line look cool!`, + titleColor: "bgCyanBright", + titleTypography: "bold", + border: false, + }); + } else { + msg({ type: "M_GENERAL", title: "Better luck next time!" }); + } + + msg({ type: "M_BAR" }); + + await endPrompt({ + title: "Thanks for playing!\n", + titleColor: "passionGradient", + titleTypography: "bold", + }); + + process.exit(0); +} + +await main().catch((error) => errorHandler(error)); diff --git a/examples/separate/columns/auto-resize.ts b/examples/other/temp/auto-resize.ts similarity index 100% rename from examples/separate/columns/auto-resize.ts rename to examples/other/temp/auto-resize.ts diff --git a/examples/separate/columns/lorem-ipsum.ts b/examples/other/temp/lorem-ipsum.ts similarity index 100% rename from examples/separate/columns/lorem-ipsum.ts rename to examples/other/temp/lorem-ipsum.ts diff --git a/examples/separate/resizer/auto-resize.ts b/examples/other/temp/resizer-tables.ts similarity index 100% rename from examples/separate/resizer/auto-resize.ts rename to examples/other/temp/resizer-tables.ts diff --git a/examples/separate/columns/responsive-table.ts b/examples/other/temp/responsive-table.ts similarity index 100% rename from examples/separate/columns/responsive-table.ts rename to examples/other/temp/responsive-table.ts diff --git a/examples/separate/columns/terminal-columns.ts b/examples/other/temp/terminal-columns.ts similarity index 100% rename from examples/separate/columns/terminal-columns.ts rename to examples/other/temp/terminal-columns.ts diff --git a/examples/separate/fallback/enquirer.ts b/examples/other/temp/wrapper.ts similarity index 91% rename from examples/separate/fallback/enquirer.ts rename to examples/other/temp/wrapper.ts index 6832eef..056a23a 100644 --- a/examples/separate/fallback/enquirer.ts +++ b/examples/other/temp/wrapper.ts @@ -3,7 +3,7 @@ import { EnquirerWrapper } from "~/fallback/enquirer.temp.js"; const longText = - "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you think about testing the very very very long text? Is something broken for you?"; + "Let's embark on a creative journey and build something completely new! Afterward, it's all yours to refine. What category best describes your project? What web technologies do you like? 42 is the answer to everything. What do you mind about testing the very very very long text? Is something broken for you?"; async function main() { const enq = new EnquirerWrapper(); diff --git a/examples/src/prompts.ts b/examples/src/prompts.ts index 7f6c6b0..614c1be 100644 --- a/examples/src/prompts.ts +++ b/examples/src/prompts.ts @@ -2,9 +2,9 @@ import { detect } from "detect-package-manager"; import { emojify } from "node-emoji"; import pc from "picocolors"; -import { anykeyPrompt, task } from "~/main.js"; +import { anykeyPrompt, spinnerTaskPrompt } from "~/main.js"; import { multiselectPrompt } from "~/main.js"; -import { progressbar } from "~/main.js"; +import { progressTaskPrompt } from "~/main.js"; import { animateText, confirmPrompt, @@ -456,7 +456,7 @@ export async function showConfirmPrompt(username: string) { if (spinner) { await showSpinner(); - await showProgressBar(); + await showProgressbar(); } // A return value is unnecessary for prompts when the result is not needed later. @@ -467,7 +467,7 @@ export async function showConfirmPrompt(username: string) { // components, as they don't return any values. export async function showSpinner() { - await task({ + await spinnerTaskPrompt({ initialMessage: "Some long-running task is in progress...", successMessage: "Hooray! The long-running task was a success!", errorMessage: "An error occurred while the long-running task!", @@ -481,8 +481,8 @@ export async function showSpinner() { }); } -export async function showProgressBar() { - await progressbar({ +export async function showProgressbar() { + await progressTaskPrompt({ total: 100, width: 10, format: diff --git a/jsr.jsonc b/jsr.jsonc index 47dbd36..4af89ed 100644 --- a/jsr.jsonc +++ b/jsr.jsonc @@ -1,6 +1,6 @@ { "name": "@reliverse/prompts", - "version": "1.4.2", + "version": "1.4.4", "author": "blefnk", "license": "MIT", "exports": "./dist-jsr/main.ts", diff --git a/package.json b/package.json index 881a0b8..c4c0a5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@reliverse/prompts", - "version": "1.4.2", + "version": "1.4.4", "author": "blefnk", "type": "module", "description": "@reliverse/prompts is a powerful library that enables seamless, typesafe, and resilient prompts for command-line applications. Crafted with simplicity and elegance, it provides developers with an intuitive and robust way to build interactive CLIs.", @@ -56,25 +56,32 @@ ], "license": "MIT", "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", "@figliolia/chalk-animation": "^1.0.4", "@sinclair/typebox": "^0.34.12", "ansi-diff-stream": "^1.2.1", "ansi-escapes": "^7.0.0", + "auto-bind": "^5.0.1", + "cli-boxes": "^4.0.1", "cli-resize": "^2.0.8", "cli-spinners": "^3.2.0", "cli-styles": "^1.0.0", "cli-truncate": "^4.0.0", "cli-width": "^4.1.0", + "code-excerpt": "^4.0.0", "colorette": "^2.0.20", + "decamelize": "^6.0.0", "defu": "^6.1.4", "detect-package-manager": "^3.0.2", "enquirer": "^2.4.1", + "es-toolkit": "^1.30.1", "external-editor": "^3.1.0", "figlet": "^1.8.0", "fs-extra": "^11.2.0", "get-pixels": "^3.3.3", "globby": "^14.0.2", "gradient-string": "^3.0.0", + "is-in-ci": "^1.0.0", "is-unicode-supported": "^2.1.0", "kleur": "^4.1.5", "log-update": "^6.1.0", @@ -84,20 +91,29 @@ "node-emoji": "^2.2.0", "nypm": "^0.4.1", "ora": "^8.1.1", + "p-map": "^7.0.3", + "patch-console": "^2.0.0", "pathe": "^1.1.2", "picocolors": "^1.1.1", + "plur": "^5.1.0", "precision": "^1.0.1", + "read-package-up": "^11.0.0", "scule": "^1.3.0", "seventh": "^0.9.2", "signal-exit": "^4.1.0", "sisteransi": "^1.0.5", + "stack-utils": "^2.0.6", "std-env": "^3.8.0", "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "terminal-size": "^4.0.0", "ts-regex-builder": "^1.8.2", + "valtio": "^2.1.2", + "widest-line": "^5.0.0", "window-size": "^1.1.1", - "wrap-ansi": "^9.0.0" + "wrap-ansi": "^9.0.0", + "yoga-wasm-web": "^0.3.3", + "zod": "^3.24.1" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.2", @@ -106,6 +122,7 @@ "@eslint/js": "^9.17.0", "@eslint/json": "^0.9.0", "@eslint/markdown": "^6.2.1", + "@faker-js/faker": "^9.3.0", "@stylistic/eslint-plugin": "^2.12.1", "@types/ansi-diff-stream": "^1.2.3", "@types/bun": "^1.1.14", @@ -121,12 +138,14 @@ "@types/window-size": "^1.1.4", "@types/wrap-ansi": "^8.1.0", "bumpp": "^9.9.1", + "delay": "^6.0.0", "eslint": "^9.17.0", "eslint-plugin-perfectionist": "^4.4.0", "execa": "^9.5.2", "jiti": "^2.4.2", "knip": "^5.41.1", "mock-stdin": "^1.0.0", + "p-queue": "^8.0.1", "printj": "^1.3.1", "sentencer": "^0.2.1", "strip-comments": "^2.0.1", diff --git a/publish.ts b/publish.ts index ff39964..11bd982 100644 --- a/publish.ts +++ b/publish.ts @@ -1,8 +1,9 @@ // 👉 usage example: `bun pub --bump=1.2.3` -import { defineCommand, errorHandler, runMain } from "@reliverse/prompts"; import { execa } from "execa"; +import { defineCommand, errorHandler, runMain } from "~/main.js"; + const main = defineCommand({ meta: { name: "pub", diff --git a/src/checkbox/index.ts b/src/checkbox/index.ts index 00c91b3..f027066 100644 --- a/src/checkbox/index.ts +++ b/src/checkbox/index.ts @@ -1,7 +1,7 @@ import ansiEscapes from "ansi-escapes"; import pc from "picocolors"; -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/confirm/confirm-main.ts b/src/confirm/confirm-main.ts index 7568982..c0a9c97 100644 --- a/src/confirm/confirm-main.ts +++ b/src/confirm/confirm-main.ts @@ -6,7 +6,8 @@ import type { ColorName, TypographyName } from "~/types/general.js"; import type { VariantName } from "~/utils/variants.js"; import { colorize } from "~/utils/colorize.js"; -import { bar, msg, msgUndoAll } from "~/utils/messages.js"; +import { bar, msg } from "~/utils/messages.js"; +import { completePrompt } from "~/utils/prompt-end.js"; import { deleteLastLine } from "~/utils/terminal.js"; export type ConfirmPromptOptions = { @@ -124,47 +125,6 @@ function renderPrompt(params: { return uiLineCount; } -/** - * Ends the prompt by optionally displaying an end message and running the action if confirmed. - * Preserves the last prompt state unless there's an endTitle. - */ -async function endPrompt( - endTitle: string, - endTitleColor: ColorName, - titleTypography: TypographyName, - titleVariant: VariantName | undefined, - border: boolean, - borderColor: ColorName, - action?: () => Promise, - value?: boolean, -): Promise { - if (action && value) { - await action(); - } - - if (endTitle) { - // Only clear previous lines if we need to show an end title - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - titleTypography, - ...(titleVariant ? { titleVariant } : {}), - border, - borderColor, - }); - } else { - // Add a bar between prompts when there's no end title - msg({ - type: "M_BAR", - borderColor, - }); - } - - return value ?? false; -} - /** * Prompts the user with a yes/no question, returning a boolean based on their input. */ @@ -206,6 +166,29 @@ export async function confirmPrompt( let lastUILineCount = 0; + function endPrompt(isCtrlC = false): void { + if (endTitle !== "") { + msg({ + type: "M_END", + title: endTitle, + titleColor: endTitleColor, + titleTypography, + ...(titleVariant ? { titleVariant } : {}), + border, + borderColor, + }); + } + rl.close(); + if (isCtrlC) { + process.exit(0); + } + } + + // Handle Ctrl+C + rl.on("SIGINT", () => { + endPrompt(true); + }); + try { while (true) { // Clear only the previous UI lines if this is a re-render @@ -264,7 +247,8 @@ export async function confirmPrompt( continue; } - return await endPrompt( + return await completePrompt( + false, endTitle, endTitleColor, titleTypography, diff --git a/src/confirm/index.ts b/src/confirm/index.ts index a2f3ab5..74837b1 100644 --- a/src/confirm/index.ts +++ b/src/confirm/index.ts @@ -1,4 +1,4 @@ -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/core/core.test.ts b/src/core/core.test.ts index 629b95b..51254f4 100644 --- a/src/core/core.test.ts +++ b/src/core/core.test.ts @@ -1,3 +1,5 @@ +import type { WriteStream } from "node:tty"; + import ansiEscapes from "ansi-escapes"; import { AsyncResource } from "node:async_hooks"; import { Stream } from "node:stream"; @@ -598,7 +600,7 @@ describe("createPrompt()", () => { process.on("warning", warningSpy); // We need to reuse the same stream to ensure it gets cleaned up properly. - const output = new WritableStream(); + const output = new WritableStream() as unknown as WriteStream; for (let i = 0; i < 15; i++) { const { answer, events } = await render( prompt, diff --git a/src/core/create-prompt.ts b/src/core/create-prompt.ts index 77f347b..064bf33 100644 --- a/src/core/create-prompt.ts +++ b/src/core/create-prompt.ts @@ -3,20 +3,21 @@ import { AsyncResource } from "node:async_hooks"; import * as readline from "node:readline"; import { onExit as onSignalExit } from "signal-exit"; -import { type Prompt, type Prettify } from "~/types/index.js"; +import { type Prompt, type PromptConfig } from "~/types/index.js"; import { type BetterReadline } from "~/types/index.js"; import { AbortPromptError, CancelPromptError, ExitPromptError, + NonInteractiveError, } from "./errors.js"; import { withHooks, effectScheduler } from "./hook-engine.js"; import { PromisePolyfill } from "./promise-polyfill.js"; import ScreenManager from "./screen-manager.js"; type ViewFunction = ( - config: Prettify, + config: Config, done: (value: Value) => void, ) => string | [string, string | undefined]; @@ -38,14 +39,39 @@ function getCallSites() { } export function createPrompt(view: ViewFunction) { - const callSites = getCallSites(); - const callerFilename = callSites[1]?.getFileName?.(); + const callerFilename = getCallSites()[0]?.getFileName() ?? "unknown"; - const prompt: Prompt = (config, context = {}) => { + const prompt: Prompt = ( + config, + context = {}, + ) => { // Default `input` to stdin - const { input = process.stdin, signal } = context; + const { input = process.stdin, signal, nonInteractive = false } = context; const cleanups = new Set<() => void>(); + // Check if terminal is interactive + if (nonInteractive || !("isTTY" in input && input.isTTY)) { + // In non-interactive mode, generate prompts.json with default values or placeholders + const promptsJson = { + type: "prompts", + message: + "Please fill this file and run the CLI again to continue with your terminal, which doesn't support interactivity (or use tty-supported terminal)", + prompts: [ + { + name: "value", + message: config.message || "Input required", + type: "input", + default: config.default || "", + }, + ], + }; + + console.log(JSON.stringify(promptsJson, null, 2)); + throw new NonInteractiveError( + "Terminal does not support interactivity. A prompts.json file has been generated.", + ); + } + // Add mute capabilities to the output const output = new MuteStream(); output.pipe(context.output ?? process.stdout); @@ -165,3 +191,11 @@ export function createPrompt(view: ViewFunction) { return prompt; } + +export type PromptOptions = { + analytics?: { + enabled?: boolean; + askConsent?: boolean; + dataCollection?: string[]; + }; +}; diff --git a/src/core/errors.ts b/src/core/errors.ts index a843e62..300d9f3 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -1,20 +1,29 @@ export class AbortPromptError extends Error { - override name = "AbortPromptError"; - override message = "Prompt was aborted"; - - constructor(options?: { cause?: unknown }) { - super(); - this.cause = options?.cause; + constructor(options?: ErrorOptions) { + super("Prompt aborted", options); + this.name = "AbortPromptError"; } } export class CancelPromptError extends Error { - override name = "CancelPromptError"; - override message = "Prompt was canceled"; + constructor() { + super("Prompt cancelled"); + this.name = "CancelPromptError"; + } } export class ExitPromptError extends Error { - override name = "ExitPromptError"; + constructor(message: string) { + super(message); + this.name = "ExitPromptError"; + } +} + +export class NonInteractiveError extends Error { + constructor(message: string) { + super(message); + this.name = "NonInteractiveError"; + } } export class HookError extends Error { diff --git a/src/core/make-theme.ts b/src/core/make-theme.ts index c17f893..a8e309b 100644 --- a/src/core/make-theme.ts +++ b/src/core/make-theme.ts @@ -1,4 +1,5 @@ -import type { Prettify, PartialDeep } from "~/types/index.js"; +import type { Prettify } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { defaultTheme, type Theme } from "./theme.js"; diff --git a/src/editor/index.ts b/src/editor/index.ts index 63c6897..d6406da 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -3,7 +3,8 @@ import type { IFileOptions } from "external-editor"; import { editAsync } from "external-editor"; import { AsyncResource } from "node:async_hooks"; -import type { PartialDeep, BetterReadline } from "~/types/index.js"; +import type { BetterReadline } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/expand/index.ts b/src/expand/index.ts index 46389a8..36e7f21 100644 --- a/src/expand/index.ts +++ b/src/expand/index.ts @@ -1,6 +1,6 @@ import pc from "picocolors"; -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/flags/main.ts b/src/flags/main.ts index 4a44aef..2c72e64 100644 --- a/src/flags/main.ts +++ b/src/flags/main.ts @@ -1,12 +1,20 @@ import type { ArgsDef, CommandDef } from "./types.js"; -import { CLIError } from "./_utils.js"; +import { NonInteractiveError } from "../core/errors.js"; +import { CLIError, resolveValue } from "./_utils.js"; import { resolveSubCommand, runCommand } from "./command.js"; import { showUsage as _showUsage } from "./usage.js"; +export type NonInteractiveAction = { + onNonInteractive: (promptsJson: any) => Promise | void; + promptsJson?: any; +}; + export type RunMainOptions = { rawArgs?: string[]; showUsage?: typeof _showUsage; + nonInteractive?: boolean; + nonInteractiveAction?: NonInteractiveAction; }; export async function runMain( @@ -15,7 +23,47 @@ export async function runMain( ) { const rawArgs = opts.rawArgs || process.argv.slice(2); const showUsage = opts.showUsage || _showUsage; + const nonInteractive = opts.nonInteractive || !process.stdin.isTTY; + const nonInteractiveAction = opts.nonInteractiveAction; + try { + // Check for non-interactive mode first + if ( + nonInteractive && + !rawArgs.includes("--help") && + !rawArgs.includes("-h") && + !rawArgs.includes("--version") + ) { + // Resolve command metadata + const meta = await resolveValue(cmd.meta || {}); + const args = await resolveValue(cmd.args || {}); + + // Generate prompts.json with command structure and defaults + const promptsJson = nonInteractiveAction?.promptsJson || { + type: "prompts", + message: + "Please fill this file and run the CLI again to continue with your terminal, which doesn't support interactivity (or use tty-supported terminal)", + command: { + name: meta.name || process.argv[1], + description: meta.description || "", + version: meta.version || "", + args: args, + }, + }; + + if (nonInteractiveAction?.onNonInteractive) { + // Execute custom non-interactive action if provided + await nonInteractiveAction.onNonInteractive(promptsJson); + process.exit(0); // Exit successfully after executing the action + } else { + // Default behavior: print JSON and throw error + console.log(JSON.stringify(promptsJson, null, 2)); + throw new NonInteractiveError( + "Terminal does not support interactivity. A prompts.json file has been generated.", + ); + } + } + if (rawArgs.includes("--help") || rawArgs.includes("-h")) { await showUsage(...(await resolveSubCommand(cmd, rawArgs))); process.exit(0); @@ -31,12 +79,16 @@ export async function runMain( } } catch (error: any) { const isCLIError = error instanceof CLIError; - if (!isCLIError) { + const isNonInteractiveError = error instanceof NonInteractiveError; + + if (!isCLIError && !isNonInteractiveError) { console.error(error, "\n"); } + if (isCLIError) { await showUsage(...(await resolveSubCommand(cmd, rawArgs))); } + console.error(error.message); process.exit(1); } diff --git a/src/flags/mod.ts b/src/flags/mod.ts index 6d96527..73f7b5d 100644 --- a/src/flags/mod.ts +++ b/src/flags/mod.ts @@ -3,6 +3,21 @@ export type { RunCommandOptions } from "./command.js"; export type { RunMainOptions } from "./main.js"; export { defineCommand, runCommand } from "./command.js"; -export { parseArgs } from "./args.js"; export { renderUsage, showUsage } from "./usage.js"; export { runMain, createMain } from "./main.js"; + +// TODO: implement global flags +export function parseArgs(_args: string[]) { + return { + // Support both long and short forms + // e.g., --help, -h + options: { + help: ["-h", "--help"], + version: ["-v", "--version"], + // Add more standard options + }, + // Support grouping of short options + // e.g., -abc equivalent to -a -b -c + allowGrouping: true, + }; +} diff --git a/src/input/index.ts b/src/input/index.ts index 346f84a..7be45af 100644 --- a/src/input/index.ts +++ b/src/input/index.ts @@ -1,4 +1,4 @@ -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/input/input-main.ts b/src/input/input-main.ts index b199333..38dcb8f 100644 --- a/src/input/input-main.ts +++ b/src/input/input-main.ts @@ -14,6 +14,7 @@ import type { SymbolName } from "~/utils/messages.js"; import type { VariantName } from "~/utils/variants.js"; import { msg, msgUndoAll, bar } from "~/utils/messages.js"; +import { completePrompt } from "~/utils/prompt-end.js"; import { deleteLastLine } from "~/utils/terminal.js"; export type InputPromptOptions = { @@ -187,20 +188,26 @@ export async function inputPrompt( const rl = readline.createInterface({ input, output }); // Graceful Ctrl+C handling: - rl.on("SIGINT", () => { - if (endTitle !== "") { - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - titleTypography, - border, - borderColor, - }); - } + async function endPrompt(isCtrlC = false) { + await completePrompt( + isCtrlC, + endTitle, + endTitleColor, + titleTypography, + titleVariant ? titleVariant : undefined, + border, + borderColor, + undefined, + false, + ); rl.close(); - process.exit(0); + if (isCtrlC) { + process.exit(0); + } + } + + rl.on("SIGINT", () => { + void endPrompt(true); }); let currentInput = hardcoded?.userInput || ""; @@ -282,19 +289,7 @@ export async function inputPrompt( // Check for Ctrl+C or stream closed if (answerInput === null) { - if (endTitle !== "") { - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - titleTypography, - border, - borderColor, - }); - } - rl.close(); - process.exit(0); + void endPrompt(true); } currentInput = answerInput.trim(); diff --git a/src/multiselect/multiselect-main.ts b/src/multiselect/multiselect-main.ts index b8df0b0..e5d2e7c 100644 --- a/src/multiselect/multiselect-main.ts +++ b/src/multiselect/multiselect-main.ts @@ -7,7 +7,8 @@ import type { ColorName, TypographyName } from "~/types/general.js"; import type { VariantName } from "~/utils/variants.js"; import { deleteLastLine } from "~/main.js"; -import { msg, msgUndoAll, symbols } from "~/utils/messages.js"; +import { msg, symbols } from "~/utils/messages.js"; +import { completePrompt } from "~/utils/prompt-end.js"; type SelectOption = { label: string; @@ -264,7 +265,7 @@ export async function multiselectPrompt( debug = false, terminalWidth: customTerminalWidth = 90, displayInstructions = false, - allowAllUnselected = true, + allowAllUnselected = false, } = params; let pointer = @@ -427,13 +428,28 @@ export async function multiselectPrompt( input.setRawMode(false); } rl.close(); - input.removeListener("keypress", onKeyPress); + input.removeListener("keypress", handleKeypress); if (isCtrlC) { process.exit(0); } } - function confirmSelection() { + async function endPrompt(isCtrlC = false) { + await completePrompt( + isCtrlC, + endTitle, + endTitleColor, + titleTypography, + titleVariant ? titleVariant : undefined, + border, + borderColor, + undefined, + false, + ); + cleanup(isCtrlC); + } + + async function confirmSelection() { if (allowAllUnselected || selectedOptions.size > 0) { const selectedValues = Array.from(selectedOptions) .filter((idx) => { @@ -450,36 +466,29 @@ export async function multiselectPrompt( resolve(selectedValues); deleteLastLine(); - msg({ type: "M_BAR", titleColor }); - - // Don't clear the final state + await completePrompt( + false, + endTitle, + endTitleColor, + titleTypography, + titleVariant ? titleVariant : undefined, + border, + borderColor, + undefined, + true, + ); } else { + deleteLastLine(); errorMessage = "You must select at least one option."; renderOptions(); } } - function endPrompt(isCtrlC = false) { - // Don't clear anything unless there's an end title - if (endTitle !== "") { - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - ...(titleTypography ? { titleTypography } : {}), - ...(titleVariant ? { titleVariant } : {}), - border, - borderColor, - }); - } - cleanup(isCtrlC); - } - - function onKeyPress(_str: string, key: readline.Key) { + function handleKeypress(_str: string, key: readline.Key): void { if (allDisabled) { if (key.name === "c" && key.ctrl) { - endPrompt(true); + void endPrompt(true); + return; } return; } @@ -518,12 +527,12 @@ export async function multiselectPrompt( } case "return": - confirmSelection(); + void confirmSelection(); break; case "c": if (key.ctrl) { - endPrompt(true); + void endPrompt(true); } break; @@ -535,6 +544,6 @@ export async function multiselectPrompt( } } - input.on("keypress", onKeyPress); + input.on("keypress", handleKeypress); }); } diff --git a/src/number/index.ts b/src/number/index.ts index a666021..54effce 100644 --- a/src/number/index.ts +++ b/src/number/index.ts @@ -1,4 +1,4 @@ -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/number/number-main.ts b/src/number/number-main.ts index e53329e..497cc05 100644 --- a/src/number/number-main.ts +++ b/src/number/number-main.ts @@ -4,7 +4,7 @@ import readline from "node:readline/promises"; import type { ColorName, TypographyName } from "~/types/general.js"; -import { msg, msgUndoAll, bar, type FmtMsgOptions } from "~/utils/messages.js"; +import { msg, bar, type FmtMsgOptions } from "~/utils/messages.js"; import { deleteLastLine, deleteLastLines } from "~/utils/terminal.js"; type VariantName = FmtMsgOptions["titleVariant"]; @@ -136,7 +136,6 @@ export async function numberPrompt(opts: NumberPromptOptions): Promise { // Graceful Ctrl+C handling: rl.on("SIGINT", () => { if (endTitle !== "") { - msgUndoAll(); msg({ type: "M_END", title: endTitle, @@ -230,7 +229,6 @@ export async function numberPrompt(opts: NumberPromptOptions): Promise { // Check for Ctrl+C or stream closed if (answerInput === null) { if (endTitle !== "") { - msgUndoAll(); msg({ type: "M_END", title: endTitle, diff --git a/src/password/index.ts b/src/password/index.ts index de479b2..3020b64 100644 --- a/src/password/index.ts +++ b/src/password/index.ts @@ -1,6 +1,6 @@ import ansiEscapes from "ansi-escapes"; -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/progressbar/ProgressBar.ts b/src/progressbar/ProgressBar.ts deleted file mode 100644 index 37553e1..0000000 --- a/src/progressbar/ProgressBar.ts +++ /dev/null @@ -1,97 +0,0 @@ -import pc from "picocolors"; - -export type ProgressBarOptions = { - total: number; // Total units of work to complete - width?: number; // Width of the progress bar - completeChar?: string; // Character to represent completed progress - incompleteChar?: string; // Character to represent incomplete progress - format?: string; // Display format of the progress bar - colorize?: boolean; // Whether to colorize the progress bar -}; - -export class ProgressBar { - private total: number; - private current: number; - private width: number; - private completeChar: string; - private incompleteChar: string; - private startTime: number; - private format: string; - private colorize: boolean; - - constructor(options: ProgressBarOptions) { - if (options.total <= 0) { - throw new Error("Total must be a positive number"); - } - - this.total = options.total; - this.current = 0; - this.width = options.width || 40; - this.completeChar = options.completeChar || "█"; - this.incompleteChar = options.incompleteChar || "░"; - this.startTime = Date.now(); - this.format = - options.format || "Progress: [:bar] :percent% | Elapsed: :elapsed s"; - this.colorize = options.colorize || false; - } - - /** - * Update the progress bar to a specific value. - * @param value - The current progress value. - */ - update(value: number) { - const newValue = Math.min(value, this.total); - if (newValue !== this.current) { - this.current = newValue; - this.render(); - } - } - - /** - * Increment the progress bar by a specific amount. - * @param amount - The amount to increment. - */ - increment(amount = 1) { - this.update(this.current + amount); - } - - /** - * Render the progress bar. - */ - private render() { - const percent = this.current / this.total; - const filledLength = Math.round(this.width * percent); - const emptyLength = this.width - filledLength; - - let bar = - this.completeChar.repeat(filledLength) + - this.incompleteChar.repeat(emptyLength); - - if (this.colorize) { - bar = - pc.green(this.completeChar.repeat(filledLength)) + - pc.red(this.incompleteChar.repeat(emptyLength)); - } - - const percentage = (percent * 100).toFixed(2); - const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2); - const eta = - percent > 0 - ? ((Date.now() - this.startTime) / percent / 1000).toFixed(2) - : "N/A"; - - const output = this.format - .replace(":bar", bar) - .replace(":percent", percentage) - .replace(":elapsed", elapsed) - .replace(":eta", eta); - - process.stdout.clearLine(0); - process.stdout.cursorTo(0); - process.stdout.write(pc.green("◆") + " " + output); - - if (this.current >= this.total) { - process.stdout.write("\n"); - } - } -} diff --git a/src/progressbar/index.ts b/src/progressbar/index.ts deleted file mode 100644 index f225583..0000000 --- a/src/progressbar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { progressbar } from "./helper.js"; -export type { ProgressBarOptions } from "./ProgressBar.js"; diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 27760f5..183f084 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -1,3 +1,7 @@ +export { NonInteractiveError } from "~/core/errors.js"; +export function isTerminalInteractive(input = process.stdin): boolean { + return Boolean(input.isTTY); +} export type { ColorName } from "~/types/general.js"; export type { ChoiceOptions } from "~/types/general.js"; export type { PromptOptions } from "~/types/general.js"; @@ -19,6 +23,8 @@ export { export { colorize } from "~/utils/colorize.js"; export { fmt, msg } from "~/utils/messages.js"; export { errorHandler } from "~/utils/errors.js"; +export { defineCommand } from "~/flags/command.js"; +export { runMain } from "~/flags/main.js"; export { colorMap } from "~/utils/mapping.js"; export { animateText } from "~/visual/animate/animate.js"; export { createAsciiArt } from "~/visual/ascii-art/ascii-art.js"; @@ -36,10 +42,9 @@ export { nextStepsPrompt } from "~/next-steps/next-steps.js"; export { numberPrompt } from "~/number/number-main.js"; export { passwordPrompt } from "~/password/password-main.js"; export { endPrompt } from "~/st-end/end.js"; -export { progressbar } from "~/progressbar/index.js"; +export * from "~/task/index.js"; export { promptsDisplayResults } from "~/results/results.js"; export { prompt } from "~/mono/mono.js"; -export { task } from "~/task/index.js"; export { default as block } from "~/block/block.js"; export { default as checkbox, Separator } from "~/checkbox/index.js"; export { default as expand } from "~/expand/index.js"; diff --git a/src/prompts/prompt.ts b/src/prompts/prompt.ts index 7f18350..4696899 100644 --- a/src/prompts/prompt.ts +++ b/src/prompts/prompt.ts @@ -9,6 +9,7 @@ import wrap from "wrap-ansi"; import type { State } from "~/types/general.js"; +import { NonInteractiveError } from "~/core/errors.js"; import { getTerminalWidth } from "~/core/utils.js"; function diffLines(a: string, b: string): number[] { @@ -56,6 +57,8 @@ export type PromptOptions = { input?: Readable; output?: Writable; debug?: boolean; + nonInteractive?: boolean; + message?: string; }; export default class Prompt { @@ -77,6 +80,7 @@ export default class Prompt { input = stdin, output = stdout, initialValue = "", + nonInteractive = false, ...opts }: PromptOptions, trackValue = true, @@ -91,6 +95,29 @@ export default class Prompt { this.input = input; this.output = output; + // Check for non-interactive mode + if (nonInteractive || !(this.input as typeof stdin).isTTY) { + // Generate prompts.json with default values or placeholders + const promptsJson = { + type: "prompts", + message: + "Please fill this file and run the CLI again to continue with your terminal, which doesn't support interactivity (or use tty-supported terminal)", + prompts: [ + { + name: "value", + message: opts.message || "Input required", + type: "input", + default: initialValue || "", + }, + ], + }; + + console.log(JSON.stringify(promptsJson, null, 2)); + throw new NonInteractiveError( + "Terminal does not support interactivity. A prompts.json file has been generated.", + ); + } + // Ensure `this.value` is always initialized, either from initialValue or as an empty string this.value = initialValue ?? ""; } diff --git a/src/rawlist/index.ts b/src/rawlist/index.ts index 0ce07f3..a65bb9e 100644 --- a/src/rawlist/index.ts +++ b/src/rawlist/index.ts @@ -1,6 +1,6 @@ import pc from "picocolors"; -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/search/index.ts b/src/search/index.ts index bcda58d..3447dfa 100644 --- a/src/search/index.ts +++ b/src/search/index.ts @@ -1,6 +1,6 @@ import pc from "picocolors"; -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/select/index.ts b/src/select/index.ts index 33f17e7..f46f651 100644 --- a/src/select/index.ts +++ b/src/select/index.ts @@ -1,7 +1,7 @@ import ansiEscapes from "ansi-escapes"; import pc from "picocolors"; -import type { PartialDeep } from "~/types/index.js"; +import type { PartialDeep } from "~/types/utils.js"; import { createPrompt, diff --git a/src/select/select-main.ts b/src/select/select-main.ts index 59a8c07..672e7a3 100644 --- a/src/select/select-main.ts +++ b/src/select/select-main.ts @@ -7,7 +7,8 @@ import type { ColorName, TypographyName } from "~/types/general.js"; import type { VariantName } from "~/utils/variants.js"; import { deleteLastLine } from "~/main.js"; -import { msg, msgUndoAll, symbols } from "~/utils/messages.js"; +import { msg, symbols } from "~/utils/messages.js"; +import { completePrompt } from "~/utils/prompt-end.js"; type SelectOption = { label: string; @@ -257,7 +258,7 @@ export async function selectPrompt( contentTypography = "italic", border = true, endTitle = "", - endTitleColor = "dim", + endTitleColor = "retroGradient", maxItems, debug = false, terminalWidth: customTerminalWidth = 90, @@ -350,10 +351,11 @@ export async function selectPrompt( }); return new Promise((resolve) => { - function onKeyPress(_str: string, key: readline.Key) { + function handleKeypress(_str: string, key: readline.Key): void { if (allDisabled) { if (key.name === "c" && key.ctrl) { - endPrompt(true); + void endPrompt(true); + return; } return; } @@ -363,9 +365,9 @@ export async function selectPrompt( } else if (key.name === "down" || key.name === "j") { moveSelectionDown(); } else if (key.name === "return") { - confirmSelection(); + void confirmSelection(); } else if (key.name === "c" && key.ctrl) { - endPrompt(true); + void endPrompt(true); } } @@ -393,7 +395,7 @@ export async function selectPrompt( renderOptions(); } - function confirmSelection() { + async function confirmSelection() { const option = options[selectedIndex]; if (!option || !isSelectOption(option)) { errorMessage = "This option is not selectable."; @@ -417,25 +419,31 @@ export async function selectPrompt( resolve(option.value); deleteLastLine(); - msg({ type: "M_BAR", borderColor }); - - // We don't clear the final state + await completePrompt( + false, + endTitle, + endTitleColor, + titleTypography, + titleVariant ? titleVariant : undefined, + border, + borderColor, + undefined, + true, + ); } - function endPrompt(isCtrlC = false) { - // Don't clear anything unless there's an end title - if (endTitle !== "") { - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - titleTypography, - ...(titleVariant ? { titleVariant } : {}), - border, - borderColor, - }); - } + async function endPrompt(isCtrlC = false) { + await completePrompt( + isCtrlC, + endTitle, + endTitleColor, + titleTypography, + titleVariant ? titleVariant : undefined, + border, + borderColor, + undefined, + false, + ); cleanup(isCtrlC); } @@ -444,13 +452,12 @@ export async function selectPrompt( input.setRawMode(false); } rl.close(); - input.removeListener("keypress", onKeyPress); - + input.removeListener("keypress", handleKeypress); if (isCtrlC) { process.exit(0); } } - input.on("keypress", onKeyPress); + input.on("keypress", handleKeypress); }); } diff --git a/src/progressbar/helper.ts b/src/task/helper.ts similarity index 73% rename from src/progressbar/helper.ts rename to src/task/helper.ts index 7ec93dd..2349349 100644 --- a/src/progressbar/helper.ts +++ b/src/task/helper.ts @@ -1,8 +1,8 @@ import { msg } from "~/utils/messages.js"; -import type { ProgressBarOptions } from "./ProgressBar.js"; +import type { ProgressBarOptions } from "./types.js"; -import { ProgressBar } from "./ProgressBar.js"; +import { progressTaskPrompt } from "./progress.js"; /** * Options for the progressbar helper function. @@ -43,31 +43,32 @@ export async function progressbar( const iterations = Math.ceil(total / increment) + 1; const delay = desiredTotalTime / iterations; - const progressBar = new ProgressBar({ + const progressBar = await progressTaskPrompt({ total, ...progressBarOptions, }); try { for (let i = 0; i <= total; i += increment) { - progressBar.update(i); + await progressBar.update(i); await sleep(delay); } - // Ensure the progress bar completes if not already if (total % increment !== 0) { - progressBar.update(total); + await progressBar.update(total); } + + msg({ + type: "M_MIDDLE", + title: "", + borderColor: "dim", + }); } catch (error) { - console.error("Progress bar encountered an error:", error); + const errorMessage = + error instanceof Error ? error.message : "Progress bar error"; + console.error("Progress bar encountered an error:", errorMessage); + throw new Error(errorMessage); } - - // New line - msg({ - type: "M_MIDDLE", - title: "", - borderColor: "dim", - }); } /** diff --git a/src/task/index.ts b/src/task/index.ts index f14c4a4..e8ef1c0 100644 --- a/src/task/index.ts +++ b/src/task/index.ts @@ -1,159 +1,5 @@ -import { type SpinnerName } from "cli-spinners"; -import process from "node:process"; -import ora from "ora"; -import pc from "picocolors"; -import { cursor, erase } from "sisteransi"; - -import { msg } from "~/utils/messages.js"; - -type SimpleSpinnerType = "default" | "dottedCircle" | "boxSpinner"; -type OraSpinnerType = Extract; -type OraAllowedSpinners = "dots" | "bouncingBar" | "arc"; - -type TaskOptions = { - initialMessage: string; - successMessage?: string; - errorMessage?: string; - delay?: number; - spinnerSolution: T; - spinnerType?: T extends "simple" ? SimpleSpinnerType : OraSpinnerType; - action: (updateMessage: (message: string) => void) => Promise; -}; - -export async function task( - options: TaskOptions, -): Promise { - const { - initialMessage, - successMessage = "Task completed successfully.", - errorMessage = "An error occurred during the task.", - delay = 100, - spinnerSolution, - spinnerType, - action, - } = options; - - let message = initialMessage; - let interval: NodeJS.Timer | null = null; - let frameIndex = 0; - - if (spinnerSolution === "ora") { - const oraSpinner = ora({ - text: initialMessage, - spinner: spinnerType as OraSpinnerType, - }); - - try { - oraSpinner.start(); - - await action((newMessage: string) => { - message = newMessage; - oraSpinner.text = newMessage; - }); - - oraSpinner.stop(); - - msg({ - type: "M_INFO", - title: successMessage, - titleColor: "cyan", - }); - } catch (error) { - oraSpinner.stopAndPersist({ - symbol: pc.red("✖"), - text: errorMessage, - }); - - msg({ - type: "M_ERROR", - title: - error instanceof Error ? error.message : "An unknown error occurred.", - titleColor: "red", - }); - - process.exit(1); - } - } else { - const simpleSpinners = { - default: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], - dottedCircle: ["○", "◔", "◑", "◕", "●"], - boxSpinner: ["▖", "▘", "▝", "▗"], - }; - - const frames = - spinnerType && spinnerType in simpleSpinners - ? simpleSpinners[spinnerType as SimpleSpinnerType] - : simpleSpinners.default; - - const handleInput = (data: Buffer) => { - const key = data.toString(); - if (key === "\r" || key === "\n") { - return; - } - }; - - try { - if ( - process.stdin.isTTY && - typeof process.stdin.setRawMode === "function" - ) { - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on("data", handleInput); - } - - interval = setInterval(() => { - const frame = pc.magenta(frames[frameIndex]); - process.stdout.write( - `${cursor.move(-999, 0)}${erase.line}${frame} ${pc.cyan(message)}`, - ); - frameIndex = (frameIndex + 1) % frames.length; - }, delay); - - await action((newMessage: string) => { - message = newMessage; - }); - - clearInterval(interval); - interval = null; - - process.stdout.write( - `\r${erase.line}${pc.green("✔")} ${successMessage}\n`, - ); - - msg({ - type: "M_INFO", - title: successMessage, - titleColor: "cyan", - }); - } catch (error) { - if (interval) { - clearInterval(interval); - } - - process.stdout.write( - `\r${erase.line}${pc.red("✖")} ${ - error instanceof Error ? errorMessage : "An unknown error occurred." - }\n`, - ); - - msg({ - type: "M_ERROR", - title: - error instanceof Error ? error.message : "An unknown error occurred.", - titleColor: "red", - }); - - process.exit(1); - } finally { - if ( - process.stdin.isTTY && - typeof process.stdin.setRawMode === "function" - ) { - process.stdin.setRawMode(false); - process.stdin.pause(); - process.stdin.removeListener("data", handleInput); - } - } - } -} +export type { ProgressBarOptions } from "./types.js"; +export type { TaskAPI, TaskGroupAPI } from "./task.js"; +export { progressTaskPrompt } from "./progress.js"; +export { spinnerTaskPrompt } from "./spinner.js"; +export { default as advancedTaskPrompt } from "./task.js"; diff --git a/src/task/progress.ts b/src/task/progress.ts new file mode 100644 index 0000000..422dac9 --- /dev/null +++ b/src/task/progress.ts @@ -0,0 +1,79 @@ +import pc from "picocolors"; +import { cursor, erase } from "sisteransi"; + +import type { ProgressBar, ProgressBarOptions } from "./types.js"; + +export async function progressTaskPrompt( + options: ProgressBarOptions, +): Promise { + if (options.total <= 0) { + throw new Error("Total must be a positive number"); + } + + const state = { + total: options.total, + current: 0, + width: options.width ?? 40, + completeChar: options.completeChar ?? "█", + incompleteChar: options.incompleteChar ?? "░", + startTime: Date.now(), + format: + options.format ?? "Progress: [:bar] :percent% | Elapsed: :elapsed s", + colorize: options.colorize ?? false, + }; + + const getElapsedTime = () => { + return ((Date.now() - state.startTime) / 1000).toFixed(2); + }; + + const getPercentage = () => { + return ((state.current / state.total) * 100).toFixed(2); + }; + + const getBar = () => { + const percent = state.current / state.total; + const filledLength = Math.round(state.width * percent); + const emptyLength = state.width - filledLength; + + const filled = state.completeChar.repeat(filledLength); + const empty = state.incompleteChar.repeat(emptyLength); + + return state.colorize ? pc.green(filled) + pc.red(empty) : filled + empty; + }; + + const render = async () => { + const bar = getBar(); + const percentage = getPercentage(); + const elapsed = getElapsedTime(); + + const output = state.format + .replace(":bar", bar) + .replace(":percent", percentage) + .replace(":elapsed", elapsed); + + process.stdout.write(cursor.move(-999, 0) + erase.line); + process.stdout.write(pc.green("◆") + " " + output); + + if (state.current >= state.total) { + process.stdout.write("\n"); + } + }; + + const update = async (value: number) => { + const newValue = Math.min(value, state.total); + if (newValue !== state.current) { + state.current = newValue; + await render(); + } + }; + + const increment = async (amount = 1) => { + await update(state.current + amount); + }; + + return { + update, + increment, + render, + }; +} diff --git a/src/task/spinner.ts b/src/task/spinner.ts new file mode 100644 index 0000000..8b7eac3 --- /dev/null +++ b/src/task/spinner.ts @@ -0,0 +1,159 @@ +import { type SpinnerName } from "cli-spinners"; +import process from "node:process"; +import ora from "ora"; +import pc from "picocolors"; +import { cursor, erase } from "sisteransi"; + +import { msg } from "~/utils/messages.js"; + +type SimpleSpinnerType = "default" | "dottedCircle" | "boxSpinner"; +type OraSpinnerType = Extract; +type OraAllowedSpinners = "dots" | "bouncingBar" | "arc"; + +type TaskOptions = { + initialMessage: string; + successMessage?: string; + errorMessage?: string; + delay?: number; + spinnerSolution: T; + spinnerType?: T extends "simple" ? SimpleSpinnerType : OraSpinnerType; + action: (updateMessage: (message: string) => void) => Promise; +}; + +export async function spinnerTaskPrompt( + options: TaskOptions, +): Promise { + const { + initialMessage, + successMessage = "Task completed successfully.", + errorMessage = "An error occurred during the task.", + delay = 100, + spinnerSolution, + spinnerType, + action, + } = options; + + let message = initialMessage; + let interval: NodeJS.Timer | null = null; + let frameIndex = 0; + + if (spinnerSolution === "ora") { + const oraSpinner = ora({ + text: initialMessage, + spinner: spinnerType as OraSpinnerType, + }); + + try { + oraSpinner.start(); + + await action((newMessage: string) => { + message = newMessage; + oraSpinner.text = newMessage; + }); + + oraSpinner.stop(); + + msg({ + type: "M_INFO", + title: successMessage, + titleColor: "cyan", + }); + } catch (error) { + oraSpinner.stopAndPersist({ + symbol: pc.red("✖"), + text: errorMessage, + }); + + msg({ + type: "M_ERROR", + title: + error instanceof Error ? error.message : "An unknown error occurred.", + titleColor: "red", + }); + + process.exit(1); + } + } else { + const simpleSpinners = { + default: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + dottedCircle: ["○", "◔", "◑", "◕", "●"], + boxSpinner: ["▖", "▘", "▝", "▗"], + }; + + const frames = + spinnerType && spinnerType in simpleSpinners + ? simpleSpinners[spinnerType as SimpleSpinnerType] + : simpleSpinners.default; + + const handleInput = (data: Buffer) => { + const key = data.toString(); + if (key === "\r" || key === "\n") { + return; + } + }; + + try { + if ( + process.stdin.isTTY && + typeof process.stdin.setRawMode === "function" + ) { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on("data", handleInput); + } + + interval = setInterval(() => { + const frame = pc.magenta(frames[frameIndex]); + process.stdout.write( + `${cursor.move(-999, 0)}${erase.line}${frame} ${pc.cyan(message)}`, + ); + frameIndex = (frameIndex + 1) % frames.length; + }, delay); + + await action((newMessage: string) => { + message = newMessage; + }); + + clearInterval(interval); + interval = null; + + process.stdout.write( + `\r${erase.line}${pc.green("✔")} ${successMessage}\n`, + ); + + msg({ + type: "M_INFO", + title: successMessage, + titleColor: "cyan", + }); + } catch (error) { + if (interval) { + clearInterval(interval); + } + + process.stdout.write( + `\r${erase.line}${pc.red("✖")} ${ + error instanceof Error ? errorMessage : "An unknown error occurred." + }\n`, + ); + + msg({ + type: "M_ERROR", + title: + error instanceof Error ? error.message : "An unknown error occurred.", + titleColor: "red", + }); + + process.exit(1); + } finally { + if ( + process.stdin.isTTY && + typeof process.stdin.setRawMode === "function" + ) { + process.stdin.setRawMode(false); + process.stdin.pause(); + process.stdin.removeListener("data", handleInput); + } + } + } +} diff --git a/src/task/task.ts b/src/task/task.ts new file mode 100644 index 0000000..468a0b3 --- /dev/null +++ b/src/task/task.ts @@ -0,0 +1,733 @@ +import type { Options } from "p-map"; + +import process from "node:process"; +import ora from "ora"; +import pMap from "p-map"; +import pc from "picocolors"; +import { cursor, erase } from "sisteransi"; +import { proxy, subscribe } from "valtio"; + +import type { ProgressBar } from "~/task/types.js"; + +import { msg } from "~/utils/messages.js"; + +import { progressTaskPrompt } from "./progress.js"; + +type SimpleSpinnerType = "default" | "dottedCircle" | "boxSpinner"; + +type State = + | "pending" + | "loading" + | "error" + | "warning" + | "success" + | "cancelled"; +type Priority = "low" | "normal" | "high" | "critical"; + +export type TaskProgress = { + current: number; + total: number; + message?: string; +}; + +export type TaskObject = { + id: string; + title: string; + state: State; + children: TaskObject[]; + status?: string; + output?: string; + error?: Error; + priority: Priority; + progress?: TaskProgress; + progressBar?: ProgressBar; + startTime?: number; + endTime?: number; + duration?: number; + cancelToken?: AbortController; +}; + +export type TaskList = TaskObject[] & { + isRoot?: boolean; +}; + +export type TaskInnerAPI = { + task: Task; + setTitle(title: string): void; + setStatus(status?: string): void; + setWarning(warning?: Error | string): void; + setError(error?: Error | string): void; + setOutput(output: string | { message: string }): void; + setProgress(progress: TaskProgress): void; + updateProgress(current: number, total?: number, message?: string): void; + cancel(): void; + isCancelled(): boolean; +}; + +export type TaskFunction = (innerApi: TaskInnerAPI) => Promise; + +export const runSymbol: unique symbol = Symbol("run"); + +export type RegisteredTask = { + [runSymbol]: () => Promise; + task: TaskObject; + clear: () => void; + cancel: () => void; +}; + +export type TaskAPI = { + result: Result; + state: State; + clear: () => void; + cancel: () => void; + progress?: TaskProgress; + duration?: number; +}; + +export type Task = (( + title: string, + options: { priority: Priority } & Partial, + taskFunction: TaskFunction, +) => Promise>) & { group: TaskGroup }; + +export type TaskGroupAPI = Results & { + clear(): void; + cancel(): void; +}; + +export type CreateTask = ( + title: string, + options: { priority: Priority } & Partial, + taskFunction: TaskFunction, +) => RegisteredTask; + +type TaskGroup = ( + options: Options & { priority?: Priority }, + createTasks: (taskCreator: CreateTask) => [...T], +) => Promise< + TaskGroupAPI< + { + [K in keyof T]: T[K] extends RegisteredTask ? TaskAPI : never; + }[number][] + > +>; + +type SpinnerType = + // Simple spinners + | "default" + | "dottedCircle" + | "boxSpinner" + // Ora spinners + | "dots" + | "bouncingBar" + | "arc"; + +type TaskDisplayType = "spinner" | "progress"; + +export type TaskOptions = { + displayType?: TaskDisplayType; + verificationSteps?: number; + stepDelay?: number; + initialDelay?: number; + exitProcessOnError?: boolean; + spinnerType?: SpinnerType; +}; + +type SpinnerTaskOptions = { + initialMessage: string; + errorMessage?: string; + delay?: number; + spinnerType?: SpinnerType; + action: (updateMessage: (message: string) => void) => Promise; +}; + +// Utility functions +function generateTaskId(): string { + return Math.random().toString(36).substring(2, 15); +} + +function arrayAdd(array: T[], element: T) { + const index = array.push(element) - 1; + return array[index]; +} + +function arrayRemove(array: T[], element: T) { + const index = array.indexOf(element); + if (index > -1) { + array.splice(index, 1); + } +} + +function registerTask( + taskList: TaskList, + taskTitle: string, + taskFunction: TaskFunction, + priority: Priority = "normal", + options: TaskOptions = {}, +): RegisteredTask { + const { + displayType = "spinner", + verificationSteps = 0, + stepDelay = 400, + initialDelay = 0, + exitProcessOnError = true, + spinnerType = "default", + } = options; + + const cancelToken = new AbortController(); + const task = arrayAdd(taskList, { + id: generateTaskId(), + title: taskTitle, + state: "pending", + children: [], + priority, + cancelToken: undefined, + }); + + task.cancelToken = cancelToken; + + // Subscribe to state changes for timing information + subscribe(task, () => { + if (task.state === "loading" && !task.startTime) { + task.startTime = Date.now(); + } else if ( + ["success", "error", "cancelled"].includes(task.state) && + task.startTime && + !task.endTime + ) { + task.endTime = Date.now(); + task.duration = task.endTime - task.startTime; + } + }); + + // Set up spinner + const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + let frameIndex = 0; + let interval: NodeJS.Timer | null = null; + + // Function to render the current state + const render = async () => { + process.stdout.write(cursor.move(-999, 0) + erase.line); + + if (displayType === "spinner") { + if ( + spinnerType === "dots" || + spinnerType === "bouncingBar" || + spinnerType === "arc" + ) { + // Use ora for these spinner types + const oraSpinner = ora({ + text: task.title + (task.status ? ` - ${task.status}` : ""), + spinner: spinnerType, + }); + oraSpinner.start(); + } else { + // Use simple spinners for the rest + const simpleSpinners = { + default: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + dottedCircle: ["○", "", "◑", "◕", "●"], + boxSpinner: ["▖", "▘", "▝", "▗"], + }; + const frames = spinnerType + ? simpleSpinners[spinnerType] + : simpleSpinners.default; + const currentFrame = pc.magenta(frames[frameIndex]); + process.stdout.write(`${currentFrame} ${pc.cyan(task.title)}`); + if (task.status) { + process.stdout.write(` - ${task.status}`); + } + } + } else { + // Render progress bar using progressTaskPrompt + if (task.progress) { + const { current, total, message } = task.progress; + if (!task.progressBar) { + task.progressBar = await progressTaskPrompt({ + total, + width: 20, + completeChar: "█", + incompleteChar: "░", + format: `:bar :percent% ${message || ""}`, + colorize: true, + }); + } + await task.progressBar.update(current); + } + } + }; + + const clearLine = () => { + if (interval) { + clearInterval(interval); + interval = null; + } + const statusIcon = + task.state === "success" + ? "✅" + : task.state === "error" || task.state === "cancelled" + ? "❌" + : ""; + + // Clear current line + process.stdout.write(cursor.move(-999, 0) + erase.line); + + // Only write final status if we have a state change + if (task.state !== "loading") { + // Write status with proper styling + if (task.state === "success") { + process.stdout.write( + `${statusIcon} ${pc.cyan(task.title)}${task.status ? ` - ${pc.greenBright(task.status)}` : ""}\n`, + ); + } else if (task.state === "error" || task.state === "cancelled") { + process.stdout.write( + `${statusIcon} ${pc.cyan(task.title)}${task.status ? ` - ${pc.red(task.status)}` : ""}\n`, + ); + if (task.error?.message) { + process.stdout.write(` ${pc.red(task.error.message)}\n`); + } + } else { + process.stdout.write( + `${statusIcon} ${pc.cyan(task.title)}${task.status ? ` - ${task.status}` : ""}\n`, + ); + } + } + }; + + // Start spinner when task starts + subscribe(task, () => { + if (task.state === "loading" && !interval) { + interval = setInterval(() => { + frameIndex = (frameIndex + 1) % frames.length; + void render(); + }, 80); + } else if (task.state !== "loading") { + clearLine(); + } + }); + + return { + task, + async [runSymbol]() { + const api = createTaskInnerApi(task); + task.state = "loading"; + + try { + if (cancelToken.signal.aborted) { + throw new Error("Task was cancelled before starting"); + } + + // Add automated verification steps + api.setStatus("Checking..."); + await new Promise((resolve) => setTimeout(resolve, initialDelay)); + + for (let i = 0; i < verificationSteps; i++) { + api.setStatus(`Verifying... (${i + 1}/${verificationSteps})`); + await new Promise((resolve) => setTimeout(resolve, stepDelay)); + } + + const taskResult = await taskFunction(api); + + if (cancelToken.signal.aborted) { + throw new Error("Task was cancelled during execution"); + } + + // If taskResult is a string, use it as the final status + if (typeof taskResult === "string") { + api.setStatus(taskResult); + } + + if (task.state === "loading") { + task.state = "success"; + } + return taskResult; + } catch (error) { + if (cancelToken.signal.aborted) { + task.state = "cancelled"; + } else if (error instanceof Error) { + api.setError(error); + } else { + api.setError(new Error(String(error))); + } + if (exitProcessOnError) { + process.exit(1); + } + throw error; + } + }, + clear() { + clearLine(); + arrayRemove(taskList, task); + }, + cancel() { + clearLine(); + cancelToken.abort(); + task.state = "cancelled"; + }, + }; +} + +// Create and export the root task list and default task function +const rootTaskList = proxy([]); + +// Add a task state manager +const taskStateManager = new Map(); + +function advancedTaskPrompt(taskList: TaskList): Task { + const task = (async ( + title: string, + options: { priority: Priority } & Partial, + taskFunction: TaskFunction, + ) => { + const { priority, ...taskOptions } = options; + const registeredTask = registerTask( + taskList, + title, + taskFunction, + priority, + taskOptions, + ); + + try { + taskStateManager.set(registeredTask.task.id, true); + const result = await registeredTask[runSymbol](); + taskStateManager.delete(registeredTask.task.id); + + return { + result, + get state() { + return registeredTask.task.state; + }, + get progress() { + return registeredTask.task.progress; + }, + get duration() { + return registeredTask.task.duration; + }, + clear: registeredTask.clear, + cancel: registeredTask.cancel, + }; + } catch (error) { + taskStateManager.delete(registeredTask.task.id); + if (registeredTask.task.cancelToken?.signal.aborted) { + return { + result: undefined, + state: "cancelled" as const, + clear: registeredTask.clear, + cancel: registeredTask.cancel, + }; + } + throw error; + } + }) as Task; + + task.group = async ( + options: Options & { priority?: Priority }, + createTasks: (taskCreator: CreateTask) => [...T], + ): Promise< + TaskGroupAPI< + { + [K in keyof T]: T[K] extends RegisteredTask + ? TaskAPI + : never; + }[number][] + > + > => { + const { + priority = "normal", + concurrency = 1, + stopOnError = true, + signal, + } = options; + const tasksQueue = createTasks((title, options, taskFunction) => { + const { priority: taskPriority = priority, ...taskOptions } = options; + return registerTask( + taskList, + title, + taskFunction, + taskPriority, + taskOptions, + ); + }); + + // Sort tasks by priority + const sortedTasks = [...tasksQueue].sort((a, b) => { + const priorityOrder = { critical: 3, high: 2, normal: 1, low: 0 }; + return priorityOrder[b.task.priority] - priorityOrder[a.task.priority]; + }); + + // Execute tasks concurrently using p-map + const results = (await pMap( + sortedTasks, + async (taskApi) => { + try { + taskStateManager.set(taskApi.task.id, true); + const result = await taskApi[runSymbol](); + taskStateManager.delete(taskApi.task.id); + + return { + result, + get state() { + return taskApi.task.state; + }, + get progress() { + return taskApi.task.progress; + }, + get duration() { + return taskApi.task.duration; + }, + clear: taskApi.clear, + cancel: taskApi.cancel, + }; + } catch (error) { + taskStateManager.delete(taskApi.task.id); + if (taskApi.task.cancelToken?.signal.aborted) { + return { + result: undefined, + state: "cancelled" as const, + clear: taskApi.clear, + cancel: taskApi.cancel, + }; + } + throw error; + } + }, + { + concurrency, + stopOnError, + signal, + }, + )) as unknown as { + [K in keyof T]: T[K] extends RegisteredTask ? TaskAPI : never; + }[number][]; + + return Object.assign(results, { + clear() { + for (const taskApi of tasksQueue) { + taskApi.clear(); + } + }, + cancel() { + for (const taskApi of tasksQueue) { + taskApi.cancel(); + } + }, + }); + }; + + return task; +} + +// Task management functionality +const createTaskInnerApi = (taskState: TaskObject): TaskInnerAPI => { + const api: TaskInnerAPI = { + task: advancedTaskPrompt(taskState.children), + setTitle(title) { + taskState.title = title; + }, + setStatus(status) { + taskState.status = status; + }, + setOutput(output) { + taskState.output = + typeof output === "string" + ? output + : "message" in output + ? output.message + : ""; + }, + setWarning(warning) { + taskState.state = "warning"; + if (warning !== undefined) { + api.setOutput(warning); + if (warning instanceof Error) { + taskState.error = warning; + } + } + }, + setError(error) { + taskState.state = "error"; + if (error !== undefined) { + api.setOutput(error); + if (error instanceof Error) { + taskState.error = error; + } + } + }, + setProgress(progress) { + taskState.progress = progress; + }, + updateProgress(current: number, total?: number, message?: string) { + const currentProgress = taskState.progress || { current: 0, total: 100 }; + taskState.progress = { + current, + total: total ?? currentProgress.total, + message: message ?? currentProgress.message, + }; + }, + cancel() { + if (taskState.cancelToken) { + taskState.cancelToken.abort(); + taskState.state = "cancelled"; + } + }, + isCancelled() { + return taskState.cancelToken?.signal.aborted || false; + }, + }; + return api; +}; + +// Update spinnerTask to use the new SpinnerType +export async function spinnerTask(options: SpinnerTaskOptions): Promise { + const { + initialMessage, + errorMessage = "An error occurred during the task.", + delay = 100, + spinnerType = "default", + action, + } = options; + + let message = initialMessage; + let interval: NodeJS.Timer | null = null; + let frameIndex = 0; + + // Use ora for its spinner types + if ( + spinnerType === "dots" || + spinnerType === "bouncingBar" || + spinnerType === "arc" + ) { + const oraSpinner = ora({ + text: initialMessage, + spinner: spinnerType, + }); + + try { + oraSpinner.start(); + + await action((newMessage: string) => { + message = newMessage; + oraSpinner.text = newMessage; + }); + + oraSpinner.stop(); + } catch (error) { + oraSpinner.stopAndPersist({ + symbol: pc.red("✖"), + text: errorMessage, + }); + + msg({ + type: "M_ERROR", + title: + error instanceof Error ? error.message : "An unknown error occurred.", + titleColor: "red", + }); + + process.exit(1); + } + } else { + // Use simple spinners + const simpleSpinners = { + default: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + dottedCircle: ["○", "", "◑", "◕", "●"], + boxSpinner: ["▖", "▘", "▝", "▗"], + }; + + const frames = + spinnerType && spinnerType in simpleSpinners + ? simpleSpinners[spinnerType as SimpleSpinnerType] + : simpleSpinners.default; + + const handleInput = (data: Buffer) => { + const key = data.toString(); + if (key === "\r" || key === "\n") { + return; + } + }; + + try { + if ( + process.stdin.isTTY && + typeof process.stdin.setRawMode === "function" + ) { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on("data", handleInput); + } + + interval = setInterval(() => { + const frame = pc.magenta(frames[frameIndex]); + process.stdout.write( + `${cursor.move(-999, 0)}${erase.line}${frame} ${pc.cyan(message)}`, + ); + frameIndex = (frameIndex + 1) % frames.length; + }, delay); + + await action((newMessage: string) => { + message = newMessage; + }); + + clearInterval(interval); + interval = null; + } catch (error) { + if (interval) { + clearInterval(interval); + } + + process.stdout.write( + `\r${erase.line}${pc.red("✖")} ${ + error instanceof Error ? errorMessage : "An unknown error occurred." + }\n`, + ); + + msg({ + type: "M_ERROR", + title: + error instanceof Error ? error.message : "An unknown error occurred.", + titleColor: "red", + }); + + process.exit(1); + } finally { + if ( + process.stdin.isTTY && + typeof process.stdin.setRawMode === "function" + ) { + process.stdin.setRawMode(false); + process.stdin.pause(); + process.stdin.removeListener("data", handleInput); + } + } + } +} + +// Export a function to get task statistics +export function getTaskStats(taskList: TaskList = rootTaskList) { + const stats = { + total: 0, + pending: 0, + loading: 0, + success: 0, + error: 0, + warning: 0, + cancelled: 0, + averageDuration: 0, + }; + + function processTask(task: TaskObject) { + stats.total++; + stats[task.state]++; + if (task.duration) { + stats.averageDuration = + (stats.averageDuration * (stats.total - 1) + task.duration) / + stats.total; + } + task.children.forEach(processTask); + } + + taskList.forEach(processTask); + return stats; +} + +export default advancedTaskPrompt(rootTaskList); diff --git a/src/task/types.ts b/src/task/types.ts new file mode 100644 index 0000000..f611457 --- /dev/null +++ b/src/task/types.ts @@ -0,0 +1,16 @@ +export type ProgressBarOptions = { + total: number; + width?: number; + completeChar?: string; + incompleteChar?: string; + format?: string; + colorize?: boolean; + increment?: number; + desiredTotalTime?: number; +}; + +export type ProgressBar = { + update: (value: number) => Promise; + increment: (amount?: number) => Promise; + render: () => Promise; +}; diff --git a/src/toggle/index.ts b/src/toggle/index.ts index 8540266..f2bf88e 100644 --- a/src/toggle/index.ts +++ b/src/toggle/index.ts @@ -6,7 +6,8 @@ import type { ColorName, TypographyName } from "~/types/general.js"; import type { VariantName } from "~/utils/variants.js"; import { deleteLastLine } from "~/main.js"; -import { msg, msgUndoAll } from "~/utils/messages.js"; +import { msg } from "~/utils/messages.js"; +import { completePrompt } from "~/utils/prompt-end.js"; export type TogglePromptParams = { title: string; @@ -218,7 +219,33 @@ export async function togglePrompt( }); return new Promise((resolve) => { - function onKeyPress(_str: string, key: readline.Key) { + function cleanup(isCtrlC = false) { + if (typeof input.setRawMode === "function") { + input.setRawMode(false); + } + rl.close(); + input.removeListener("keypress", handleKeypress); + if (isCtrlC) { + process.exit(0); + } + } + + function endPrompt(isCtrlC = false) { + if (endTitle !== "") { + msg({ + type: "M_END", + title: endTitle, + titleColor: endTitleColor, + titleTypography, + ...(titleVariant ? { titleVariant } : {}), + border, + borderColor, + }); + } + cleanup(isCtrlC); + } + + function handleKeypress(_str: string, key: readline.Key): void { if (key.name === "left" || key.name === "h") { selectedIndex = (selectedIndex - 1 + options.length) % options.length; errorMessage = ""; @@ -236,54 +263,26 @@ export async function togglePrompt( cleanup(); // Return boolean: first option = true, second option = false const booleanValue = selectedIndex === 0; - - // Only clear and show end title if one is provided - if (endTitle !== "") { - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - titleTypography, - ...(titleVariant ? { titleVariant } : {}), - border, - borderColor, - }); - } + void completePrompt( + false, + endTitle, + endTitleColor, + titleTypography, + titleVariant ? titleVariant : undefined, + border, + borderColor, + undefined, + booleanValue, + ); resolve(booleanValue); - deleteLastLine(); msg({ type: "M_BAR", borderColor }); } } else if (key.name === "c" && key.ctrl) { - // Only clear and show end title if one is provided - if (endTitle !== "") { - msgUndoAll(); - msg({ - type: "M_END", - title: endTitle, - titleColor: endTitleColor, - titleTypography, - ...(titleVariant ? { titleVariant } : {}), - border, - borderColor, - }); - } - cleanup(true); - } - } - - function cleanup(isCtrlC = false) { - if (typeof input.setRawMode === "function") { - input.setRawMode(false); - } - rl.close(); - input.removeListener("keypress", onKeyPress); - if (isCtrlC) { - process.exit(0); + endPrompt(true); } } - input.on("keypress", onKeyPress); + input.on("keypress", handleKeypress); }); } diff --git a/src/types/index.ts b/src/types/index.ts index 0bb597a..ed08696 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,42 @@ -export * from "./readline.js"; -export * from "./utils.js"; +import type MuteStream from "mute-stream"; +import type { ReadStream } from "node:fs"; +import type { Interface as ReadLine } from "node:readline"; +import type { Writable } from "node:stream"; +import type { ReadableStream } from "node:stream/web"; + +export type BetterReadline = { + input: ReadStream & { fd: 0 }; + output: MuteStream; + line: string; + cursor: number; + clearLine(dir: number): void; +} & ReadLine; + +export type Context = { + input?: (ReadStream & { fd: 0 }) | ReadableStream | MuteStream; + output?: NodeJS.WriteStream | Writable; + signal?: AbortSignal; + clearPromptOnDone?: boolean; + nonInteractive?: boolean; +}; + +export type PromptConfig = { + message?: string; + default?: string | number | boolean; + theme?: Theme; + [key: string]: any; +}; + +export type Prompt< + Value = unknown, + Config extends PromptConfig = PromptConfig, +> = ( + config: Prettify, + context?: Context, +) => Promise & { cancel: () => void }; + +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +export type State = "initial" | "active" | "loading" | "done" | "error"; diff --git a/src/utils/component.ts b/src/utils/component.ts index 07212e7..5ffefa3 100644 --- a/src/utils/component.ts +++ b/src/utils/component.ts @@ -25,7 +25,7 @@ const S_STEP_CANCEL = s("■", "x"); const S_STEP_ERROR = s("▲", "x"); const S_STEP_SUBMIT = s("◇", "o"); -const S_BAR_START = s("┌", "T"); +const S_BAR_START = s("╭", "T"); const S_BAR = s("│", "|"); const S_BAR_END = s("└", "—"); diff --git a/src/utils/messages.ts b/src/utils/messages.ts index d8591db..969f610 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -17,7 +17,7 @@ import { isValidVariant, } from "~/utils/variants.js"; -import { getTerminalWidth } from "../core/utils.js"; +import { getExactTerminalWidth, getTerminalWidth } from "../core/utils.js"; /** * Known symbol names that will have IntelliSense support @@ -75,14 +75,15 @@ const u = (c: string, fallback: string) => (unicode ? c : fallback); export const symbols = { pointer: u("👉", ">"), - start: u("╭", "T"), + start: u("╭", "*"), middle: u("│", "|"), end: u("╰", "*"), line: u("─", "—"), - corner_top_right: u("����", "T"), + corner_top_right: u("╭", "*"), step_active: u("◆", "♦"), step_error: u("🗴", "x"), info: u("ℹ", "i"), + success: u("✅", "✓"), } as const; /** @@ -199,17 +200,18 @@ export function fmt(opts: FmtMsgOptions): { text: string; lineCount: number } { ? colorMap[opts.borderColor](symbols.end + symbols.line) : symbols.end + symbols.line; + const lineLength = + opts.horizontalLineLength === 0 + ? getExactTerminalWidth() - 3 + : (opts.horizontalLineLength ?? getExactTerminalWidth() - 3); + const suffixStartLine = opts.borderColor - ? colorMap[opts.borderColor]( - `${symbols.line.repeat(opts.horizontalLineLength ?? 23)}⊱`, - ) - : `${symbols.line.repeat(opts.horizontalLineLength ?? 23)}⊱`; + ? colorMap[opts.borderColor](`${symbols.line.repeat(lineLength)}⊱`) + : `${symbols.line.repeat(lineLength)}⊱`; const suffixEndLine = opts.borderColor - ? colorMap[opts.borderColor]( - `${symbols.line.repeat(opts.horizontalLineLength ?? 23)}⊱`, - ) - : `${symbols.line.repeat(opts.horizontalLineLength ?? 23)}⊱`; + ? colorMap[opts.borderColor](`${symbols.line.repeat(lineLength)}⊱`) + : `${symbols.line.repeat(lineLength)}⊱`; const MSG_CONFIGS: Record = { M_NULL: { @@ -453,8 +455,10 @@ export function fmt(opts: FmtMsgOptions): { text: string; lineCount: number } { } // For M_END with border, format the title line if (opts.type === "M_END" && opts.border && index === 0) { - // Title line with info symbol - return `${pc.green(symbols.info)} ${line}`; + // Use middle bar if title is empty, otherwise use info icon + return !opts.title + ? `${borderWithSpace}${line}` + : `${pc.green(symbols.info)} ${line}`; } // Skip if line already has a bar or is empty if (!line.trim() || line.includes(symbols.middle)) { @@ -468,7 +472,7 @@ export function fmt(opts: FmtMsgOptions): { text: string; lineCount: number } { }); if (opts.type === "M_END" && opts.border) { - lines.push(`${prefixEndLine}${suffixEndLine}`); + lines.push(`${prefixEndLine}${suffixEndLine}\n`); } const finalText = lines.join("\n"); diff --git a/src/utils/platforms.ts b/src/utils/platforms.ts index e2c4698..00f81a9 100644 --- a/src/utils/platforms.ts +++ b/src/utils/platforms.ts @@ -99,7 +99,7 @@ export const pmv = await getNpmVersion(pm); // export const pkg = packageJson; export const pkg = { name: "@reliverse/prompts", - version: "1.4.2", + version: "1.4.4", description: "@reliverse/prompts is a powerful library that enables seamless, typesafe, and resilient prompts for command-line applications. Crafted with simplicity and elegance, it provides developers with an intuitive and robust way to build interactive CLIs.", }; diff --git a/src/utils/prompt-end.ts b/src/utils/prompt-end.ts new file mode 100644 index 0000000..0331ab1 --- /dev/null +++ b/src/utils/prompt-end.ts @@ -0,0 +1,43 @@ +import type { ColorName, TypographyName } from "~/types/general.js"; +import type { VariantName } from "~/utils/variants.js"; + +import { msg } from "~/utils/messages.js"; + +/** + * Ends the prompt by optionally displaying an end message and running the action if confirmed. + * Preserves the last prompt state unless there's an endTitle. + */ +export async function completePrompt( + isCtrlC: boolean, + endTitle: string, + endTitleColor: ColorName, + titleTypography: TypographyName, + titleVariant: VariantName | undefined, + border: boolean, + borderColor: ColorName, + action?: () => Promise, + value?: boolean, +): Promise { + if (action && value) { + await action(); + } + + if (isCtrlC) { + msg({ + type: "M_END", + title: endTitle, + titleColor: endTitleColor, + titleTypography, + ...(titleVariant ? { titleVariant } : {}), + border, + borderColor, + }); + } else { + msg({ + type: "M_BAR", + borderColor, + }); + } + + return value ?? false; +} diff --git a/src/utils/prompt-tmp.ts b/src/utils/prompt-tmp.ts index 5921bcb..44856b7 100644 --- a/src/utils/prompt-tmp.ts +++ b/src/utils/prompt-tmp.ts @@ -94,8 +94,8 @@ export function createPrompt( const _render = () => renderFn.call(self); function prompt() { - const sink = new WriteStream(0); - sink._write = (_chunk, _encoding, done) => { + const stream = new WriteStream(0); + stream._write = (_chunk, _encoding, done) => { if (_track) { value = _rl.line.replace(/\t/g, ""); _cursor = _rl.cursor; @@ -103,11 +103,11 @@ export function createPrompt( } done(); }; - _input.pipe(sink); + _input.pipe(stream); _rl = readline.createInterface({ input: _input, - output: sink, + output: stream, tabSize: 2, prompt: "", escapeCodeTimeout: 50, diff --git a/src/utils/prompt-two.ts b/src/utils/prompt-two.ts index 5921bcb..f36fcbd 100644 --- a/src/utils/prompt-two.ts +++ b/src/utils/prompt-two.ts @@ -68,10 +68,17 @@ export function createPrompt( render: renderFn, input = stdin, output = stdout, + debug = process.env["DEBUG"] === "true", ...opts }: PromptOptions, trackValue = true, ) { + if (debug) { + console.debug("Terminal info:", { + isTTY: process.stdout.isTTY, + columns: process.stdout.columns, + }); + } const _input = input; const _output = output; let _rl!: ReadLine; @@ -94,8 +101,8 @@ export function createPrompt( const _render = () => renderFn.call(self); function prompt() { - const sink = new WriteStream(0); - sink._write = (_chunk, _encoding, done) => { + const stream = new WriteStream(0); + stream._write = (_chunk, _encoding, done) => { if (_track) { value = _rl.line.replace(/\t/g, ""); _cursor = _rl.cursor; @@ -103,11 +110,11 @@ export function createPrompt( } done(); }; - _input.pipe(sink); + _input.pipe(stream); _rl = readline.createInterface({ input: _input, - output: sink, + output: stream, tabSize: 2, prompt: "", escapeCodeTimeout: 50, diff --git a/tsconfig.json b/tsconfig.json index fedc71a..892c98d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,11 +7,11 @@ "#/*": ["./addons/*"], "@/*": ["./examples/*"] }, - "target": "ES2022", + "target": "ES2023", "module": "NodeNext", "moduleDetection": "force", "moduleResolution": "nodenext", - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": ["DOM", "DOM.Iterable", "ES2023"], "resolveJsonModule": true, "verbatimModuleSyntax": true, "isolatedModules": true,