@reliverse/rempts 1.7.65 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1534 -1431
  3. package/cleanup.mjs +33 -0
  4. package/dist/cancel.d.ts +31 -0
  5. package/dist/cancel.js +28 -0
  6. package/dist/ffi.d.ts +1 -0
  7. package/dist/ffi.js +165 -0
  8. package/dist/group.d.ts +16 -0
  9. package/dist/group.js +22 -0
  10. package/dist/launcher/command.d.ts +8 -0
  11. package/dist/launcher/command.js +10 -0
  12. package/dist/launcher/discovery.d.ts +3 -0
  13. package/dist/launcher/discovery.js +207 -0
  14. package/dist/launcher/errors.d.ts +15 -0
  15. package/dist/launcher/errors.js +31 -0
  16. package/dist/launcher/help.d.ts +3 -0
  17. package/dist/launcher/help.js +145 -0
  18. package/dist/launcher/mod.d.ts +12 -0
  19. package/dist/launcher/mod.js +222 -0
  20. package/dist/launcher/parser.d.ts +14 -0
  21. package/dist/launcher/parser.js +255 -0
  22. package/dist/launcher/registry.d.ts +10 -0
  23. package/dist/launcher/registry.js +42 -0
  24. package/dist/launcher/types.d.ts +78 -0
  25. package/dist/launcher/validator.d.ts +3 -0
  26. package/dist/launcher/validator.js +39 -0
  27. package/dist/mod.d.ts +6 -0
  28. package/dist/mod.js +6 -0
  29. package/dist/prompt.d.ts +13 -0
  30. package/dist/prompt.js +53 -0
  31. package/dist/selection.d.ts +92 -0
  32. package/dist/selection.js +191 -0
  33. package/dist/spinner.d.ts +26 -0
  34. package/dist/spinner.js +141 -0
  35. package/dist/utils.d.ts +3 -0
  36. package/dist/utils.js +11 -0
  37. package/package.json +41 -47
  38. package/bin/libs/animate/animate-mod.ts.txt +0 -78
  39. package/bin/libs/anykey/anykey-mod.d.ts +0 -12
  40. package/bin/libs/anykey/anykey-mod.js +0 -125
  41. package/bin/libs/cancel/cancel.d.ts +0 -45
  42. package/bin/libs/cancel/cancel.js +0 -72
  43. package/bin/libs/confirm/confirm-alias.d.ts +0 -2
  44. package/bin/libs/confirm/confirm-alias.js +0 -2
  45. package/bin/libs/confirm/confirm-mod.d.ts +0 -5
  46. package/bin/libs/confirm/confirm-mod.js +0 -179
  47. package/bin/libs/date/date.d.ts +0 -2
  48. package/bin/libs/date/date.js +0 -254
  49. package/bin/libs/editor/editor-mod.d.ts +0 -25
  50. package/bin/libs/editor/editor-mod.js +0 -1133
  51. package/bin/libs/figures/figures-mod.d.ts +0 -461
  52. package/bin/libs/figures/figures-mod.js +0 -285
  53. package/bin/libs/group/group-mod.d.ts +0 -33
  54. package/bin/libs/group/group-mod.js +0 -89
  55. package/bin/libs/input/input-alias.d.ts +0 -5
  56. package/bin/libs/input/input-alias.js +0 -4
  57. package/bin/libs/input/input-mod.d.ts +0 -16
  58. package/bin/libs/input/input-mod.js +0 -370
  59. package/bin/libs/intro/intro-alias.d.ts +0 -3
  60. package/bin/libs/intro/intro-alias.js +0 -3
  61. package/bin/libs/intro/intro-mod.d.ts +0 -19
  62. package/bin/libs/intro/intro-mod.js +0 -71
  63. package/bin/libs/launcher/command-runner.d.ts +0 -31
  64. package/bin/libs/launcher/command-runner.js +0 -229
  65. package/bin/libs/launcher/launcher-alias.d.ts +0 -2
  66. package/bin/libs/launcher/launcher-alias.js +0 -2
  67. package/bin/libs/launcher/launcher-mod.d.ts +0 -66
  68. package/bin/libs/launcher/launcher-mod.js +0 -1037
  69. package/bin/libs/launcher/launcher-types.d.ts +0 -176
  70. package/bin/libs/launcher/launcher-types.js +0 -0
  71. package/bin/libs/log/log-alias.d.ts +0 -1
  72. package/bin/libs/log/log-alias.js +0 -2
  73. package/bin/libs/msg-fmt/colors.d.ts +0 -30
  74. package/bin/libs/msg-fmt/colors.js +0 -42
  75. package/bin/libs/msg-fmt/mapping.d.ts +0 -3
  76. package/bin/libs/msg-fmt/mapping.js +0 -41
  77. package/bin/libs/msg-fmt/messages.d.ts +0 -35
  78. package/bin/libs/msg-fmt/messages.js +0 -305
  79. package/bin/libs/msg-fmt/terminal.d.ts +0 -15
  80. package/bin/libs/msg-fmt/terminal.js +0 -60
  81. package/bin/libs/msg-fmt/variants.d.ts +0 -11
  82. package/bin/libs/msg-fmt/variants.js +0 -52
  83. package/bin/libs/multiselect/multiselect-alias.d.ts +0 -2
  84. package/bin/libs/multiselect/multiselect-alias.js +0 -2
  85. package/bin/libs/multiselect/multiselect-prompt.d.ts +0 -2
  86. package/bin/libs/multiselect/multiselect-prompt.js +0 -340
  87. package/bin/libs/next-steps/next-steps.d.ts +0 -13
  88. package/bin/libs/next-steps/next-steps.js +0 -24
  89. package/bin/libs/number/number-mod.d.ts +0 -28
  90. package/bin/libs/number/number-mod.js +0 -234
  91. package/bin/libs/outro/outro-alias.d.ts +0 -3
  92. package/bin/libs/outro/outro-alias.js +0 -3
  93. package/bin/libs/outro/outro-mod.d.ts +0 -7
  94. package/bin/libs/outro/outro-mod.js +0 -49
  95. package/bin/libs/reliarg/reliarg-mod.d.ts +0 -76
  96. package/bin/libs/reliarg/reliarg-mod.js +0 -276
  97. package/bin/libs/results/results.d.ts +0 -7
  98. package/bin/libs/results/results.js +0 -27
  99. package/bin/libs/select/nummultiselect-prompt.d.ts +0 -6
  100. package/bin/libs/select/nummultiselect-prompt.js +0 -141
  101. package/bin/libs/select/numselect-prompt.d.ts +0 -7
  102. package/bin/libs/select/numselect-prompt.js +0 -111
  103. package/bin/libs/select/select-alias.d.ts +0 -9
  104. package/bin/libs/select/select-alias.js +0 -9
  105. package/bin/libs/select/select-prompt.d.ts +0 -5
  106. package/bin/libs/select/select-prompt.js +0 -311
  107. package/bin/libs/select/toggle-prompt.d.ts +0 -5
  108. package/bin/libs/select/toggle-prompt.js +0 -207
  109. package/bin/libs/spinner/spinner-impl.d.ts +0 -70
  110. package/bin/libs/spinner/spinner-impl.js +0 -336
  111. package/bin/libs/spinner/spinner-mod.d.ts +0 -167
  112. package/bin/libs/spinner/spinner-mod.js +0 -447
  113. package/bin/libs/utils/colorize.d.ts +0 -2
  114. package/bin/libs/utils/colorize.js +0 -122
  115. package/bin/libs/utils/errors.d.ts +0 -1
  116. package/bin/libs/utils/errors.js +0 -17
  117. package/bin/libs/utils/prevent.d.ts +0 -8
  118. package/bin/libs/utils/prevent.js +0 -62
  119. package/bin/libs/utils/prompt-end.d.ts +0 -8
  120. package/bin/libs/utils/prompt-end.js +0 -36
  121. package/bin/libs/utils/stream-text.d.ts +0 -18
  122. package/bin/libs/utils/stream-text.js +0 -138
  123. package/bin/libs/utils/system.d.ts +0 -6
  124. package/bin/libs/utils/system.js +0 -7
  125. package/bin/libs/utils/validate.d.ts +0 -21
  126. package/bin/libs/utils/validate.js +0 -17
  127. package/bin/libs/visual/visual-mod.ts.txt +0 -19
  128. package/bin/mod.d.ts +0 -50
  129. package/bin/mod.js +0 -127
  130. package/bin/types.d.ts +0 -372
  131. /package/{bin → dist/launcher}/types.js +0 -0
package/README.md CHANGED
@@ -1,1431 +1,1534 @@
1
- # 📃 rempts • powerful js/ts cli builder
2
-
3
- [sponsor](https://github.com/sponsors/blefnk) — [discord](https://discord.gg/Pb8uKbwpsJ) — [repo](https://github.com/reliverse/rempts) — [npm](https://npmjs.com/@reliverse/rempts)
4
-
5
- > @reliverse/rempts is a modern, type-safe toolkit for building delightful cli experiences. it's fast, flexible, and made for developer happiness. file-based commands keep things simple—no clutter, just clean and easy workflows. this is how cli should feel.
6
-
7
- ## Features
8
-
9
- - 😘 drop-in to libraries like `unjs/citty` and `@clack/prompts`
10
- - 📝 includes comprehensive set of built-in cli prompts
11
- - 📂 file-based commands (app-router style by default)
12
- - 🫂 rempts keeps you from fighting with your CLI tool
13
- - 🏎️ prompt engine that *feels* modern — and actually is
14
- - ✨ rempts is your end-to-end CLI UI + command framework
15
- - 🌿 multi-level file-based subcommands (sibling + nested)
16
- - 💪 built for DX precision and high-context terminal UX
17
- - 🎭 looks great in plain scripts or full CLI apps
18
- - 🎨 customizable themes and styled output
19
- - 📦 built-in output formatter and logger
20
- - 🚨 crash-safe (Ctrl+C, SIGINT, errors)
21
- - ⚡ blazing-fast, zero runtime baggage
22
- - 🧩 router + argument parser built-in
23
- - 🧠 type-safe from args to prompts
24
- - 📐 smart layout for small terminals
25
- - 🎛️ override styles via prompt options
26
- - 🪄 minimal API surface, maximum expressiveness
27
- - 🧪 scriptable for testing, stable for production
28
- - 🏞️ no more hacking together `inquirer`/`citty`/`commander`/`chalk`
29
- - 🆕 automatic command creation (`bun dler rempts --init cmd1 cmd2`)
30
- - 🐦‍🔥 automatic creation of `src/app/cmds.ts` file (`bun dler rempts`)
31
-
32
- ## Installation
33
-
34
- ```bash
35
- bun add @reliverse/rempts
36
- ```
37
-
38
- ## Usage Examples
39
-
40
- - [Prompts](#prompts)
41
- - [Launcher](#launcher)
42
-
43
- ## Screenshot
44
-
45
- ![Rempts Example CLI Screenshot](./example/example.png)
46
-
47
- ## API Overview
48
-
49
- All main prompts APIs are available from the package root:
50
-
51
- ```ts
52
- import {
53
- // ...prompts
54
- inputPrompt, selectPrompt, multiselectPrompt, numberPrompt,
55
- confirmPrompt, togglePrompt,
56
- startPrompt, endPrompt, resultPrompt, nextStepsPrompt,
57
- // ...hooks
58
- createSpinner,
59
- // ...launcher
60
- createCli, defineCommand, defineArgs,
61
- // ...types
62
- // ...more
63
- } from "@reliverse/rempts";
64
- ```
65
-
66
- > See [`src/mod.ts`](./src/mod.ts) for the full list of exports.
67
-
68
- ## Prompts
69
-
70
- ### Built-in Prompts
71
-
72
- | Prompt | Description |
73
- |---------------------------|-----------------------------------------------------------|
74
- | `createSpinner` | Start/stop spinner |
75
- | `inputPrompt` | Single-line input (with mask support, e.g. for passwords) |
76
- | `selectPrompt` | Single-choice radio menu |
77
- | `multiselectPrompt` | Multi-choice checkbox menu |
78
- | `numberPrompt` | Type-safe number input |
79
- | `confirmPrompt` | Yes/No toggle |
80
- | `togglePrompt` | Custom on/off toggles |
81
- | `resultPrompt` | Show results in a styled box |
82
- | `nextStepsPrompt` | Show next steps in a styled list |
83
- | `startPrompt`/`endPrompt` | Makes CLI start/end flows look nice |
84
- | `datePrompt` | Date input with format validation |
85
- | `anykeyPrompt` | Wait for any keypress |
86
-
87
- ### Aliases
88
-
89
- To help you migrate from the different CLI frameworks, `@reliverse/rempts` has some aliases for the most popular prompts.
90
-
91
- | Prompt | Aliases |
92
- |-----------------------|------------------|
93
- | `createCli` | `runMain` |
94
- | `onCmdInit` | `setup` |
95
- | `onCmdExit` | `cleanup` |
96
- | `createSpinner` | `spinner` |
97
- | `selectPrompt` | `select` |
98
- | `multiselectPrompt` | `multiselect` |
99
- | `inputPrompt` | `text`, `input` |
100
- | `confirmPrompt` | `confirm` |
101
- | `introPrompt` | `intro`, `start` |
102
- | `outroPrompt` | `outro`, `end` |
103
- | `log` | `relinka` |
104
-
105
- ### Prompts Usage Example
106
-
107
- ```ts
108
- import { relinka } from "@reliverse/relinka";
109
-
110
- import {
111
- startPrompt,
112
- inputPrompt,
113
- selectPrompt,
114
- defineCommand,
115
- runMain
116
- } from "@reliverse/rempts";
117
-
118
- async function main() {
119
- await startPrompt({ title: "Project Setup" });
120
-
121
- const name = await inputPrompt({
122
- title: "What's your project name?",
123
- defaultValue: "my-cool-project",
124
- });
125
-
126
- const spinner = createSpinner({
127
- text: "Loading...",
128
- indicator: "timer", // or "dots"
129
- frames: ["", "◐", "◓", ""], // custom frames
130
- delay: 80, // custom delay
131
- onCancel: () => {
132
- console.log("Operation cancelled");
133
- },
134
- cancelMessage: "Operation cancelled by user",
135
- errorMessage: "Operation failed",
136
- signal: abortController.signal,
137
- }).start();
138
-
139
- // The spinner will show:
140
- // ◒ Loading... [5s]
141
- // With animated frames and timer
142
-
143
- const framework = await selectPrompt({
144
- title: "Pick your framework",
145
- options: [
146
- { value: "next", label: "Next.js" },
147
- { value: "svelte", label: "SvelteKit" },
148
- { value: "start", label: "TanStack Start" },
149
- ],
150
- defaultValue: "next",
151
- });
152
-
153
- console.log("Your result:", { name, framework });
154
- };
155
-
156
- await main();
157
- ```
158
-
159
- **Available spinner options:**
160
-
161
- | Option | Description |
162
- |--------|-------------|
163
- | `cancelMessage` | The message to display when the spinner is cancelled |
164
- | `color` | The color of the spinner |
165
- | `delay` | The delay between frames |
166
- | `errorMessage` | The message to display when the spinner fails |
167
- | `failText` | The text to display when the spinner fails |
168
- | `frames` | The frames to use for the spinner |
169
- | `hideCursor` | Whether to hide the cursor |
170
- | `indicator` | The indicator to use for the spinner |
171
- | `onCancel` | The function to call when the spinner is cancelled |
172
- | `prefixText` | The text to display before the spinner |
173
- | `signal` | The signal to use for the spinner |
174
- | `silent` | Whether to hide the spinner |
175
- | `spinner` | The spinner to use for the spinner |
176
- | `successText` | The text to display when the spinner succeeds |
177
- | `text` | The text to display next to the spinner |
178
-
179
- **Available indicator options:**
180
-
181
- | Option | Description |
182
- |--------|-------------|
183
- | `timer` | The timer indicator |
184
- | `dots` | The dots indicator |
185
-
186
- **Available signal options:**
187
-
188
- | Option | Description |
189
- |--------|-------------|
190
- | `abortController.signal` | The signal to use for the spinner |
191
-
192
- **Available frames options:**
193
-
194
- | Option | Description |
195
- |--------|-------------|
196
- | `["◒", "◐", "◓", "◑"]` | The frames to use for the spinner |
197
-
198
- **Available delay options:**
199
-
200
- | Option | Description |
201
- |--------|-------------|
202
- | `80` | The delay between frames |
203
-
204
- **Available onCancel options:**
205
-
206
- | Option | Description |
207
- |--------|-------------|
208
- | `() => { console.log("Operation cancelled"); }` | The function to call when the spinner is cancelled |
209
-
210
- ## Launcher
211
-
212
- > **Note**: `runMain` is now an alias for `createCli` and is still supported for backward compatibility. The new `createCli` API provides a more intuitive object-based configuration format.
213
-
214
- ### Automatic command creation
215
-
216
- ```bash
217
- bun add -D @reliverse/dler
218
- bun dler rempts --init cmd1 cmd2 # creates `src/app/cmd1/cmd.ts` and `src/app/cmd2/cmd.ts` files
219
- bun dler rempts # creates `src/app/cmds.ts` file
220
- ```
221
-
222
- ### Terminology
223
-
224
- - **Launcher/Router**: The main entry point for your CLI. Visit [CLI Launcher (Router)](#cli-launcher-router) section to learn more.
225
- - **Command**: A command is a function that defines the inner script launched by the main script where runMain() is used or by some other command.
226
- - **Argument**: An argument is a value that is passed to a command.
227
- - **Flag**: A flag is a boolean argument that is used to enable or disable a feature.
228
- - **Option**: An option is a named argument that is used to configure a command.
229
-
230
- #### Launcher Usage Example
231
-
232
- **Important**: Ensure your commands don't have `await main();`, `await createCli();`, or something like that to prevent any unexpected behavior. Only main command should have it.
233
-
234
- ```ts
235
- import { relinka } from "@reliverse/relinka";
236
-
237
- import { defineCommand, createCli } from "@reliverse/rempts";
238
-
239
- const main = defineCommand({
240
- meta: {
241
- name: "rempts",
242
- version: "1.0.0",
243
- description: "Rempts Launcher Playground CLI",
244
- },
245
- onCmdInit() {
246
- relinka("success", "Setup");
247
- },
248
- onCmdExit() {
249
- relinka("success", "Cleanup");
250
- },
251
- commands: {
252
- build: () => import("./app/build/cmd.js").then((r) => r.default),
253
- deploy: () => import("./app/deploy/cmd.js").then((r) => r.default),
254
- debug: () => import("./app/debug/cmd.js").then((r) => r.default),
255
- },
256
- });
257
-
258
- // New object format (recommended)
259
- await createCli({
260
- mainCommand: main,
261
- fileBased: {
262
- enable: true,
263
- cmdsRootPath: "my-cmds", // default is `./app`
264
- },
265
- // Optionally disable auto-exit to handle errors manually:
266
- autoExit: false,
267
- });
268
-
269
- // Legacy format (still supported)
270
- await createCli(main, {
271
- fileBased: {
272
- enable: true,
273
- cmdsRootPath: "my-cmds", // default is `./app`
274
- },
275
- // Optionally disable auto-exit to handle errors manually:
276
- autoExit: false,
277
- });
278
- ```
279
-
280
- This flexibility allows you to easily build a rich, multi-command CLI with minimal boilerplate. The launcher even supports nested commands, making it simple to construct complex CLI applications.
281
-
282
- #### File-Based Commands
283
-
284
- Drop a `./src/cli/app/add/index.ts` and it's live.
285
-
286
- ```ts
287
- import { defineArgs, defineCommand } from "@reliverse/rempts";
288
- export default defineCommand({
289
- meta: {
290
- name: "add",
291
- version: "1.0.0",
292
- description: "Add stuff to your project",
293
- },
294
- args: {
295
- name: defineArgs({ // 💡 PRO TIP: use defineArgs() to get fully correct intellisense
296
- type: "string",
297
- required: true,
298
- description: "Name of what to add",
299
- }),
300
- },
301
- async run({ args }) {
302
- relinka("log", "Adding:", args.name);
303
- },
304
- });
305
- ```
306
-
307
- **Supports**:
308
-
309
- - `arg-cmdName.{ts,js}`,
310
- - `cmdName/index.{ts,js}`,
311
- - `cmdName/cmdName-mod.{ts,js}`,
312
- - **Multi-level subcommands:** `foo/bar/baz/cmd.ts` `my-cli foo bar baz`
313
- - And more — with automatic usage output.
314
-
315
- **Hint**:
316
-
317
- - Install `bun add -D @reliverse/dler`
318
- - Use `bun dler rempts --init cmd1 cmd2` to init commands for rempts launcher's automatically
319
-
320
- ### Advanced Launcher Usage
321
-
322
- ```ts
323
- defineCommand({
324
- meta: { name: "cli", version: "1.0.0" },
325
- args: {
326
- name: { type: "string", required: true },
327
- verbose: { type: "boolean", default: false },
328
- animals: { type: "array", default: ["cat","dog"] },
329
- },
330
- async run({ args, raw }) { // or `async run(ctx)`
331
- relinka("log", args.name, args.verbose, args.animals); // or `relinka("log", ctx.args.name, ...);`
332
- },
333
- });
334
- ```
335
-
336
- **Supports**:
337
-
338
- - `positional` args
339
- - `array` types (`--tag foo --tag bar`)
340
- - Default values, validations, descriptions
341
- - Full help rendering from metadata
342
-
343
- **By the way! Multi-level subcommands!**
344
-
345
- You can also nest subcommands arbitrarily deep:
346
-
347
- ```bash
348
- app/
349
- foo/
350
- bar/
351
- baz/
352
- cmd.ts
353
- ```
354
-
355
- Invoke with:
356
-
357
- ```bash
358
- my-cli foo bar baz --some-flag
359
- ```
360
-
361
- The launcher will recursively traverse subfolders for each non-flag argument, loading the deepest `cmd.ts`/`cmd.js` it finds, and passing the remaining arguments to it.
362
-
363
- See [example/launcher/app/nested](./example/launcher/app/nested/) and [example/launcher/app/sibling](./example/launcher/app/sibling/) folders to learn more.
364
-
365
- When playing with the example, you can run e.g. `bun dev:modern nested foo bar baz` to see the result in action.
366
-
367
- ### Playground
368
-
369
- ```bash
370
- git clone https://github.com/reliverse/rempts
371
- cd rempts
372
- bun i
373
- bun dev
374
- ```
375
-
376
- - `bun dev:prompts`: This example will show you a `multiselectPrompt()` where you can choose which CLI prompts you want to play with.
377
- - `bun dev:modern`: This example will show you a modern CLI launcher usage with file-based commands.
378
- - `bun dev:classic`: This example will show you a classic CLI launcher usage with programmatic commands.
379
-
380
- ### Launcher Usage Examples
381
-
382
- #### Minimal Usage Example
383
-
384
- **1 Create a `src/mod.ts` file:**
385
-
386
- ```ts
387
- import { createCli, defineCommand } from "@reliverse/rempts";
388
-
389
- // New object format (recommended)
390
- await createCli({
391
- mainCommand: defineCommand({}),
392
- });
393
-
394
- // Legacy format (still supported)
395
- await createCli(defineCommand({}));
396
- ```
397
-
398
- **2 Run the following:**
399
-
400
- ```bash
401
- bun add -D @reliverse/dler
402
- bun dler rempts --init my-cmd-1 # or: dler rempts --init my-cmd-1 my-cmd-2 --main src/mod.ts
403
- # * `--main` is optional, default is `./src/mod.ts`
404
- # * you can specify multiple commands at once
405
- ```
406
-
407
- **3 Visit `src/app/my-cmd-1/mod.ts` and edit it:**
408
-
409
- ```ts
410
- export default defineCommand({
411
- run() { console.log("Hello, world!"); },
412
- });
413
- ```
414
-
415
- **4. Test it:**
416
-
417
- ```bash
418
- bun src/mod.ts
419
- ```
420
-
421
- #### Medium Usage Example
422
-
423
- ```ts
424
- import { defineCommand, createCli } from "@reliverse/rempts";
425
-
426
- const main = defineCommand({
427
- meta: {
428
- name: "mycli",
429
- },
430
- run() {
431
- console.log("Happy, Reliversing!");
432
- },
433
- });
434
-
435
- // New object format (recommended)
436
- await createCli({
437
- mainCommand: main,
438
- });
439
-
440
- // Legacy format (still supported)
441
- await createCli(main);
442
- ```
443
-
444
- #### Classic Usage Example
445
-
446
- ```ts
447
- import { relinka } from "@reliverse/relinka";
448
-
449
- import {
450
- startPrompt,
451
- inputPrompt,
452
- selectPrompt,
453
- defineCommand,
454
- createCli
455
- } from "@reliverse/rempts";
456
-
457
- const main = defineCommand({
458
- meta: {
459
- name: "mycli",
460
- version: "1.0.0",
461
- description: "CLI powered by Rempts",
462
- },
463
- args: {
464
- name: {
465
- type: "string",
466
- required: true,
467
- description: "The name of the project",
468
- },
469
- },
470
- async run({ args }) {
471
- await startPrompt({
472
- title: "Project Setup",
473
- });
474
-
475
- const name = await inputPrompt({
476
- title: "What's your project name?",
477
- placeholder: args.name,
478
- });
479
-
480
- const framework = await selectPrompt({
481
- title: "Pick your framework",
482
- options: [
483
- { value: "next", label: "Next.js" },
484
- { value: "svelte", label: "SvelteKit" },
485
- { value: "start", label: "TanStack Start" },
486
- ],
487
- });
488
-
489
- relinka("log", "You have selected:", { name, framework });
490
- },
491
- });
492
-
493
- // New object format (recommended)
494
- await createCli({
495
- mainCommand: main,
496
- });
497
-
498
- // Legacy format (still supported)
499
- await createCli(main);
500
- ```
501
-
502
- #### Advanced Usage Example
503
-
504
- ```ts
505
- import { relinka } from "@reliverse/relinka";
506
-
507
- import {
508
- startPrompt,
509
- inputPrompt,
510
- selectPrompt,
511
- defineCommand,
512
- runMain,
513
- } from "@reliverse/rempts";
514
-
515
- /**
516
- * Main command defined using `defineCommand()`.
517
- *
518
- * This command demonstrates the full range of launcher features along with all supported argument types:
519
- *
520
- * - Global Usage Handling: Automatically processes `--help` and `--version`.
521
- * - File-Based Commands: Scans "app" for commands (e.g., `init`).
522
- * - Comprehensive Argument Parsing: Supports positional, boolean, string, number, and array arguments.
523
- * - Interactive Prompts: Uses built-in prompt functions for an engaging CLI experience.
524
- */
525
- const mainCommand = defineCommand({
526
- meta: {
527
- name: "rempts",
528
- version: "1.6.0",
529
- description:
530
- "An example CLI that supports file-based commands and all argument types.",
531
- },
532
- args: {
533
- // Positional arguments
534
- inputFile: {
535
- type: "positional",
536
- description: "Path to the input file (only for the main command).",
537
- },
538
- config: {
539
- type: "positional",
540
- description: "Path to the configuration file.",
541
- },
542
- // Boolean arguments
543
- verbose: {
544
- type: "boolean",
545
- default: false,
546
- description: "Whether to print verbose logs in the main command.",
547
- },
548
- debug: {
549
- type: "boolean",
550
- default: false,
551
- description: "Enable debug mode for additional logging.",
552
- },
553
- // String argument
554
- name: {
555
- type: "string",
556
- description: "The name of the project.",
557
- },
558
- // Number argument
559
- timeout: {
560
- type: "number",
561
- default: 30,
562
- description: "Timeout in seconds for the CLI operation.",
563
- },
564
- // Array argument
565
- tags: {
566
- type: "array",
567
- default: ["cli", "rempts"],
568
- description: "List of tags associated with the project.",
569
- },
570
- },
571
- async run({ args, raw }) {
572
- // Display invocation details and parsed arguments.
573
- relinka("log", "Main command was invoked!");
574
- relinka("log", "Parsed main-command args:", args);
575
- relinka("log", "Raw argv:", raw);
576
- relinka("log", "\nHelp: `rempts --help`, `rempts cmdName --help`");
577
-
578
- // Begin interactive session with a prompt.
579
- await startPrompt({
580
- title: "Project Setup",
581
- });
582
-
583
- // Ask for the project name, falling back to provided argument or a default.
584
- const projectName = await inputPrompt({
585
- title: "What's your project name?",
586
- placeholder: args.name ?? "my-cool-cli",
587
- });
588
-
589
- // Let the user pick a framework from a select prompt.
590
- const framework = await selectPrompt({
591
- title: "Pick your framework",
592
- options: [
593
- { value: "next", label: "Next.js" },
594
- { value: "svelte", label: "SvelteKit" },
595
- { value: "start", label: "TanStack Start" },
596
- ],
597
- });
598
-
599
- // Log all gathered input details.
600
- relinka("log", "You have selected:", {
601
- projectName,
602
- framework,
603
- inputFile: args.inputFile,
604
- config: args.config,
605
- verbose: args.verbose,
606
- debug: args.debug,
607
- timeout: args.timeout,
608
- tags: args.tags,
609
- });
610
- },
611
- });
612
-
613
- /**
614
- * The `createCli()` function sets up the launcher with several advanced features:
615
- *
616
- * - File-Based Commands: Enables scanning for commands within the "app" directory.
617
- * - Alias Mapping: Shorthand flags (e.g., `-v`) are mapped to their full names (e.g., `--verbose`).
618
- * - Strict Mode & Unknown Flag Warnings: Unknown flags are either warned about or handled via a callback.
619
- * - Negated Boolean Support: Allows flags to be negated (e.g., `--no-verbose`).
620
- * - Custom Unknown Flag Handler: Provides custom handling for unrecognized flags.
621
- */
622
- // New object format (recommended)
623
- await createCli({
624
- mainCommand: mainCommand,
625
- fileBased: {
626
- enable: true, // Enables file-based command detection.
627
- cmdsRootPath: "app", // Directory to scan for commands.
628
- },
629
- alias: {
630
- v: "verbose", // Maps shorthand flag -v to --verbose.
631
- },
632
- strict: false, // Do not throw errors for unknown flags.
633
- warnOnUnknown: false, // Warn when encountering unknown flags.
634
- negatedBoolean: true, // Support for negated booleans (e.g., --no-verbose).
635
- // unknown: (flagName) => {
636
- // relinka("warn", "Unknown flag encountered:", flagName);
637
- // return false;
638
- // },
639
- });
640
-
641
- // Legacy format (still supported)
642
- await createCli(mainCommand, {
643
- fileBased: {
644
- enable: true, // Enables file-based command detection.
645
- cmdsRootPath: "app", // Directory to scan for commands.
646
- },
647
- alias: {
648
- v: "verbose", // Maps shorthand flag -v to --verbose.
649
- },
650
- strict: false, // Do not throw errors for unknown flags.
651
- warnOnUnknown: false, // Warn when encountering unknown flags.
652
- negatedBoolean: true, // Support for negated booleans (e.g., --no-verbose).
653
- // unknown: (flagName) => {
654
- // relinka("warn", "Unknown flag encountered:", flagName);
655
- // return false;
656
- // },
657
- });
658
- ```
659
-
660
- ### CLI Launcher (Router)
661
-
662
- Finally, a full-featured CLI launcher without the ceremony. `@reliverse/rempts`'s so called "launcher" is a uniquely powerful and ergonomic CLI toolkit—one that helps you build delightful developer experiences with less code and more confidence. The launcher supports both programmatically defined commands and file-based routing, so you can structure your CLI however you like. It automatically detects and loads commands from your filesystem and provides robust usage and error handling out-of-the-box. The launcher is more than just a command runner—it's a robust, developer-friendly engine with several advanced features and thoughtful design choices:
663
-
664
- - **File-Based & Defined Commands:**
665
- Use `commands` in your command definition or let the launcher automatically load commands from a specified directory.
666
-
667
- - **Automatic Command Detection:**
668
- The launcher scans your specified `cmdsRootPath` for command files matching common patterns such as:
669
- - `arg-cmdName.{ts,js}`
670
- - `cmdName/index.{ts,js}`
671
- - `cmdName/cmdName-mod.{ts,js}`
672
- - And more — with automatic usage output if a command file is not found.
673
-
674
- - **Built-In Flag Handling:**
675
- Automatically processes global flags such as:
676
- - `--help` and `-h` to show usage details.
677
- - `--version` and `-v` to display version information.
678
- - `--debug` for verbose logging during development.
679
-
680
- - **Unified Argument Parsing:**
681
- Seamlessly combines positional and named arguments with zero configuration, auto-parsing booleans, strings, numbers, arrays, and even supporting negated flags like `--no-flag`.
682
-
683
- - **Customizable Behavior:**
684
- Options such as `fileBased.enable`, `cmdsRootPath`, and `autoExit` allow you to tailor the launcher's behavior. For example, you can choose whether the process should exit automatically on error or allow manual error handling.
685
-
686
- - **Error Management & Usage Output:**
687
- The launcher provides clear error messages for missing required arguments, invalid types, or command import issues, and it automatically displays usage information for your CLI.
688
-
689
- - **Lifecycle Hooks:**
690
- You can define optional lifecycle hooks in your main command:
691
- - `onLauncherInit` and `onLauncherExit` (global, called once per CLI process)
692
- - `onCmdInit` and `onCmdExit` (per-command, called before/after each command, but NOT for the main `run()` handler)
693
-
694
- **Global Hooks:**
695
- - `onLauncherInit`: Called once, before any command/run() is executed.
696
- - `onLauncherExit`: Called once, after all command/run() logic is finished (even if an error occurs).
697
-
698
- **Per-Command Hooks:**
699
- - `onCmdInit`: Called before each command (not for main `run()`).
700
- - `onCmdExit`: Called after each command (not for main `run()`).
701
-
702
- This means:
703
- - If your CLI has multiple commands, `onCmdInit` and `onCmdExit` will be called for each command invocation, not just once for the whole CLI process.
704
- - If your main command has a `run()` handler (and no command is invoked), these hooks are **not** called; use the `run()` handler itself or the global hooks for such logic.
705
- - This allows you to perform setup/teardown logic specific to each command execution.
706
- - If you want logic to run only once for the entire CLI process, use `onLauncherInit` and `onLauncherExit`.
707
-
708
- **Example:**
709
-
710
- ```ts
711
- const main = defineCommand({
712
- onLauncherInit() { relinka('info', 'Global setup (once per process)'); },
713
- onLauncherExit() { relinka('info', 'Global cleanup (once per process)'); },
714
- onCmdInit() { relinka('info', 'Setup for each command'); },
715
- onCmdExit() { relinka('info', 'Cleanup for each command'); },
716
- commands: { ... },
717
- run() { relinka('info', 'Main run handler (no command)'); },
718
- });
719
- // onLauncherInit/onLauncherExit are called once per process
720
- // onCmdInit/onCmdExit are called for every command (not for main run())
721
- // If you want per-run() logic, use the run() handler or global hooks
722
- ```
723
-
724
- - **Deprecation Notice**
725
- - The legacy `setup` and `cleanup` names are still supported as aliases for per-command hooks, but will be removed in a future major version. Prefer `onCmdInit` and `onCmdExit` going forward.
726
- - The `subCommands` property is deprecated as well. Please use `commands` instead. `subCommands` will be removed in a future major version.
727
-
728
- - **Dynamic Usage Examples:**
729
- - The launcher inspects your available commands and their argument definitions, then prints a plausible example CLI invocation for a random command directly in the help output. This helps users understand real-world usage at a glance.
730
-
731
- - **File-Based & Programmatic Commands:**
732
- - Both file-based and object commands are fully supported. The launcher can introspect their argument definitions and metadata for help, usage, and validation.
733
- - File-based commands are auto-discovered from your filesystem, while programmatic commands can be defined inline in your main command.
734
-
735
- - **Context-Aware Help Output:**
736
- - The help/usage output adapts to your CLI's structure, showing available commands, their aliases, argument details, and even dynamic usage examples. It also displays global options and context-specific error messages.
737
-
738
- - **Error Handling:**
739
- - The launcher provides clear, actionable error messages for missing required arguments, invalid types, unknown commands, and import errors. It always shows relevant usage information to help users recover quickly.
740
-
741
- - **Unified Argument Parsing:**
742
- - All arguments (positional, named, boolean, string, number, array) are parsed and validated automatically. Negated flags (like `--no-flag`) are supported out of the box.
743
-
744
- - **Extensible & Flexible:**
745
- - The launcher is highly extensible. You can use it with both Bun and Node.js, and it works seamlessly with both file-based and programmatic command definitions. You can also customize its behavior with options like `autoExit`, `cmdsRootPath`, and more.
746
-
747
- - **Bun & Node.js Support:**
748
- - The launcher is designed to work in both Bun and Node.js environments, so you can use it in any modern JavaScript/TypeScript project.
749
-
750
- - **Prompt-First, Modern UX:**
751
- - The launcher integrates tightly with the prompt engine, so you can build interactive, delightful CLIs with minimal effort.
752
-
753
- ### Launcher Programmatic Execution
754
-
755
- For larger CLIs or when you want to programmatically run commands (e.g.: [prompt demo](./example/prompts/mod.ts), tests, etc), you can organize your commands in a `cmds.ts` file and use the `runCmd` utility. Example:
756
-
757
- ```ts
758
- // example/launcher/app/runcmd/cmd.ts
759
-
760
- import { relinka } from "@reliverse/relinka";
761
- import { defineArgs, defineCommand, runCmd } from "@reliverse/rempts";
762
- import { cmdMinimal } from "../cmds";
763
-
764
- export default defineCommand({
765
- meta: {
766
- name: "runcmd",
767
- description:
768
- "Demonstrate how to use runCmd() to invoke another command programmatically.",
769
- },
770
- args: defineArgs({
771
- name: {
772
- type: "string",
773
- description: "your name",
774
- },
775
- }),
776
- async run({ args }) {
777
- // const username = args.name ?? "Alice";
778
- const username = args.name; // intentionally missing fallback
779
- relinka(
780
- "info",
781
- `Running the 'minimal' command using runCmd() with name='${username}'`,
782
- );
783
- await runCmd(await cmdMinimal(), ["--name", username]);
784
- relinka("log", "Done running 'minimal' via runCmd().");
785
- },
786
- });
787
- ```
788
-
789
- ### Using `runCmd` with Flexible Argument Handling
790
-
791
- The `runCmd` function supports flexible argument passing, automatically normalizing template literals and space-separated strings:
792
-
793
- ```ts
794
- import { runCmd } from "@reliverse/rempts";
795
-
796
- // Traditional way - each argument as separate array element
797
- await runCmd(cmd, ["--dev", "true", "--name", "John"]);
798
-
799
- // Template literals work automatically
800
- await runCmd(cmd, [`--dev ${isDev}`]); // Automatically converted to ["--dev", "true"]
801
- await runCmd(cmd, [`--dev ${isDev} --build mod.ts`]); // ["--dev", "true", "--build", "mod.ts"]
802
-
803
- // Mixed arrays with template literals and regular strings
804
- await runCmd(cmd, [
805
- `--dev ${isDev} --build mod.ts`,
806
- "--pub true",
807
- "--someBoolean",
808
- ]);
809
-
810
- // Multiple template literals
811
- await runCmd(cmd, [`--dev ${isDev}`, `--name ${userName}`, `--count ${count}`]);
812
- ```
813
-
814
- **Remember**:
815
-
816
- - If you need to pass a value with spaces (e.g. a name like "John Doe"), you should quote it in your template literal: `await runCmd(cmd, ['--name "John Doe"']);`
817
- - Otherwise, it will be split into two arguments: `"John"` and `"Doe"`.
818
- - We do not handle this intentionally, because some library users might rely on this Node.js behavior and handle it themselves in their own way (e.g. space can serve as a separator for values).
819
-
820
- ### Loading Commands with `loadCommand`
821
-
822
- The `loadCommand` utility helps you load command files from your filesystem. It automatically handles:
823
-
824
- - Relative paths (both `./build` and `build` work the same)
825
- - Automatic detection of `cmd.{ts,js}` files
826
- - Clear error messages when files are not found
827
-
828
- ```ts
829
- import { loadCommand } from "@reliverse/rempts";
830
-
831
- // These are equivalent:
832
- const cmd1 = await loadCommand("./build"); // Looks for build/cmd.ts or build/cmd.js
833
- const cmd2 = await loadCommand("build"); // Same as above
834
- const cmd3 = await loadCommand("./build/cmd"); // Explicit path to cmd file
835
-
836
- // You can then use the loaded command with runCmd:
837
- await runCmd(cmd1, ["--some-flag"]);
838
- ```
839
-
840
- ```ts
841
- // src/app/cmds.ts
842
- export const getBuildCmd = async (): Promise<Command> => loadCommand("./build");
843
-
844
- // src/cli.ts
845
- import { runCmd } from "@reliverse/rempts";
846
- import { getBuildCmd } from "./app/cmds";
847
- await runCmd(await getBuildCmd(), ["--prod"]);
848
- ```
849
-
850
- **Error Handling:**
851
- If the command file is not found, you'll get a clear error message:
852
-
853
- ```bash
854
- No command file found in /path/to/build. Expected to find either:
855
- - /path/to/build/cmd.ts
856
- - /path/to/build/cmd.js
857
- Please ensure one of these files exists and exports a default command.
858
- ```
859
-
860
- **Best Practices:**
861
-
862
- - Use `loadCommand` when you need to load commands from the filesystem
863
- - Use `runCmd` to execute the loaded command with arguments
864
- - Keep your command files in a consistent location (e.g., `src/app/yourCmdName/cmd.ts`)
865
- - Export commands from a central file like `src/app/cmds.ts` for better organization
866
-
867
- ```ts
868
- // example/launcher/app/cmds.ts
869
- import { loadCommand } from "@reliverse/rempts";
870
-
871
- export async function getBuildCmd() {
872
- return loadCommand("./build");
873
- }
874
-
875
- export async function getDeployCmd() {
876
- return loadCommand("./deploy");
877
- }
878
-
879
- // Usage:
880
- import { getBuildCmd } from "./cmds";
881
- const buildCmd = await getBuildCmd();
882
- await runCmd(buildCmd, ["--prod"]);
883
- ```
884
-
885
- ```ts
886
- // example/launcher/app/minimal/cmd.ts
887
-
888
- import { relinka } from "@reliverse/relinka";
889
- import { defineArgs, defineCommand } from "@reliverse/rempts";
890
-
891
- export default defineCommand({
892
- meta: {
893
- name: "minimal",
894
- description: "hello world",
895
- },
896
- args: defineArgs({
897
- name: {
898
- type: "string",
899
- description: "your name",
900
- required: true,
901
- },
902
- }),
903
- run({ args }) {
904
- relinka("success", `👋 Hello, ${args.name}!`);
905
- },
906
- });
907
- ```
908
-
909
- ### Using `runCmdWithSubcommands` for Subcommands and Nested Subcommands
910
-
911
- If you need to programmatically run commands that support subcommands (including nested subcommands), use `runCmdWithSubcommands`:
912
-
913
- ```ts
914
- import { runCmdWithSubcommands } from "@reliverse/rempts";
915
-
916
- // Single-level subcommand
917
- await runCmdWithSubcommands(mainCmd, [`build --input src/mod.ts --someBoolean`]);
918
-
919
- // Subcommand with positional arguments
920
- await runCmdWithSubcommands(mainCmd, [`build src/mod.ts --someBoolean`]);
921
-
922
- // Nested subcommands
923
- await runCmdWithSubcommands(mainCmd, [`build someSubCmd src/mod.ts --no-cjs`]);
924
- await runCmdWithSubcommands(mainCmd, [`build sub1 sub2 sub3 file.ts --flag`]);
925
-
926
- // Mixed array with subcommands
927
- await runCmdWithSubcommands(mainCmd, [
928
- `build someSubCmd src/mod.ts`,
929
- "--no-cjs",
930
- "--verbose"
931
- ]);
932
- ```
933
-
934
- **Note:**
935
-
936
- - `runCmdWithSubcommands` automatically normalizes template literals and space-separated strings, just like `runCmd`.
937
- - If you need to pass a value with spaces (e.g. a name like "John Doe"), you should quote it in your template literal: `await runCmdWithSubcommands(cmd, ['--name "John Doe"']);`
938
- - For subcommands, always use `runCmdWithSubcommands` for the most robust behavior.
939
-
940
- ## Argument Types: Usage Comparison
941
-
942
- Below is a demonstration of how to define and use all supported argument types in rempts: positional, boolean, string, number, and array. This includes example CLI invocations and the resulting parsed output.
943
-
944
- ```ts
945
- import { defineCommand, createCli } from "@reliverse/rempts";
946
-
947
- const main = defineCommand({
948
- meta: {
949
- name: "mycli",
950
- version: "1.0.0",
951
- description: "Demo of all argument types",
952
- },
953
- args: {
954
- // Positional argument (required)
955
- input: {
956
- type: "positional",
957
- required: true,
958
- description: "Input file path",
959
- },
960
- // Boolean flag (default: false)
961
- verbose: {
962
- type: "boolean",
963
- default: false,
964
- description: "Enable verbose output",
965
- },
966
- // String option (optional)
967
- name: {
968
- type: "string",
969
- description: "Your name",
970
- },
971
- // Number option (optional, with default)
972
- count: {
973
- type: "number",
974
- default: 1,
975
- description: "How many times to run",
976
- },
977
- // Array option (can be repeated, accepts any value)
978
- tags: {
979
- type: "array",
980
- default: ["demo"],
981
- description: "Tags for this run (repeatable)",
982
- },
983
- },
984
- run({ args }) {
985
- console.log("Parsed args:", args);
986
- },
987
- });
988
-
989
- // New object format (recommended)
990
- await createCli({
991
- mainCommand: main,
992
- });
993
-
994
- // Legacy format (still supported)
995
- await createCli(main);
996
- ```
997
-
998
- ### Example CLI Invocations
999
-
1000
- #### 1. Positional argument
1001
-
1002
- ```bash
1003
- mycli input.txt
1004
- # args.input = "input.txt"
1005
- ```
1006
-
1007
- #### 2. Boolean flag
1008
-
1009
- ```bash
1010
- mycli input.txt --verbose
1011
- # → args.verbose = true
1012
- mycli input.txt --no-verbose
1013
- # → args.verbose = false
1014
- ```
1015
-
1016
- #### 3. String option
1017
-
1018
- ```bash
1019
- mycli input.txt --name Alice
1020
- # → args.name = "Alice"
1021
- mycli input.txt
1022
- # args.name = undefined
1023
- ```
1024
-
1025
- #### 4. Number option
1026
-
1027
- ```bash
1028
- mycli input.txt --count 5
1029
- # args.count = 5
1030
- mycli input.txt
1031
- # args.count = 1 (default)
1032
- ```
1033
-
1034
- #### 5. Array option (repeatable, accepts any value)
1035
-
1036
- You can provide array values using any of the following syntaxes (mix and match as needed):
1037
-
1038
- - Repeated flags:
1039
-
1040
- ```bash
1041
- mycli input.txt --tags foo --tags bar --tags baz
1042
- # args.tags = ["foo", "bar", "baz"]
1043
- ```
1044
-
1045
- - Comma-separated values (with or without spaces):
1046
-
1047
- ```bash
1048
- mycli input.txt --tags foo,bar,baz
1049
- mycli input.txt --tags foo, bar, baz
1050
- # args.tags = ["foo", "bar", "baz"]
1051
- ```
1052
-
1053
- - Bracketed values (must be passed as a single argument!):
1054
-
1055
- ```bash
1056
- mycli input.txt --tags "[foo,bar,baz]"
1057
- # → args.tags = ["foo", "bar", "baz"]
1058
- ```
1059
-
1060
- - Mix and match:
1061
-
1062
- ```bash
1063
- mycli input.txt --tags foo --tags "[bar,bar2,bar3]" --tags baz
1064
- # → args.tags = ["foo", "bar", "bar2", "bar3", "baz"]
1065
- ```
1066
-
1067
- > **Important:**
1068
- >
1069
- > - **Quoted values (single or double quotes around elements) are NOT supported and will throw an error.**
1070
- > - Example: `--tags 'foo'` or `--tags "[\"bar\",'baz']"` will throw an error.
1071
- > - **Bracketed or comma-separated lists must be passed as a single argument.**
1072
- > - Example: `--tags "[foo,bar]"` (quotes around the whole value, not around elements)
1073
- > - If you split a bracketed value across arguments, you will get a warning or incorrect parsing.
1074
- > - **Shells remove quotes before passing arguments to the CLI.** If you want to pass a value with commas or brackets, always quote the whole value.
1075
- > - **Troubleshooting:**
1076
- > - If you see a warning about possible shell splitting, try quoting the whole value: `--tags "[a,b,c]"`
1077
- > - If you see an error about quoted values, remove quotes around individual elements.
1078
-
1079
- **Example error:**
1080
-
1081
- ```bash
1082
- $ bun example/launcher/modern.ts build --entry "[foo.ts," "bar.ts]"
1083
- ✖ Don't use quotes around array elements.
1084
- ✖ Also — don't use spaces — unless you wrap the whole array in quotes.
1085
- ⚠ Array argument --entry: Detected possible shell splitting of bracketed value ('[foo.ts,').
1086
- ⚠ If you intended to pass a bracketed list, quote the whole value like: --entry "[a, b, c]"
1087
- ```
1088
-
1089
- #### 7. All together
1090
-
1091
- ```bash
1092
- mycli input.txt --verbose --name Alice --count 3 --tags foo --tags bar
1093
- # → args = {
1094
- # input: "input.txt",
1095
- # verbose: true,
1096
- # name: "Alice",
1097
- # count: 3,
1098
- # tags: ["foo", "bar"]
1099
- # }
1100
- ```
1101
-
1102
- #### 8. Value Validation with `allowed`
1103
-
1104
- All argument types support an optional `allowed` property that restricts which values can be passed:
1105
-
1106
- ```ts
1107
- const main = defineCommand({
1108
- args: {
1109
- // Only allow specific string values
1110
- mode: {
1111
- type: "string",
1112
- allowed: ["development", "production", "test"],
1113
- description: "The mode to run in"
1114
- },
1115
-
1116
- // Only allow specific boolean values (e.g. if you only want true)
1117
- force: {
1118
- type: "boolean",
1119
- allowed: [true],
1120
- description: "Force the operation"
1121
- },
1122
-
1123
- // Only allow specific numbers
1124
- level: {
1125
- type: "number",
1126
- allowed: [1, 2, 3],
1127
- description: "The level to use"
1128
- },
1129
-
1130
- // Only allow specific values in an array
1131
- tags: {
1132
- type: "array",
1133
- allowed: ["web", "api", "mobile"],
1134
- description: "Tags to apply"
1135
- },
1136
-
1137
- // Only allow specific positional values
1138
- action: {
1139
- type: "positional",
1140
- allowed: ["build", "serve", "test"],
1141
- description: "The action to perform"
1142
- }
1143
- }
1144
- });
1145
- ```
1146
-
1147
- If someone tries to pass a value that's not in the `allowed` list, they'll get a helpful error message:
1148
-
1149
- ```bash
1150
- mycli --mode staging
1151
- # Error: Invalid value for --mode: staging. Allowed values are: development, production, test
1152
-
1153
- mycli --level 4
1154
- # Error: Invalid value for --level: 4. Allowed values are: 1, 2, 3
1155
-
1156
- mycli --tags desktop
1157
- # Error: Invalid value in array --tags: desktop. Allowed values are: web, api, mobile
1158
- ```
1159
-
1160
- The validation happens after type casting, so for example with numbers, the input will first be converted to a number and then checked against the allowed list.
1161
-
1162
- ## Typed Commands System
1163
-
1164
- The typed commands system provides TypeScript intellisense and type safety for rempts launcher usage while maintaining dynamic code execution.
1165
-
1166
- - 🎯 **TypeScript Intellisense**: Full autocomplete for command names and arguments
1167
- - 🔒 **Type Safety**: Compile-time checking for argument types and required fields
1168
- - **Dynamic Execution**: Commands are still loaded and executed dynamically
1169
- - 📝 **Automatic Sync**: Utility script to keep types in sync with actual command definitions
1170
-
1171
- ### Usage
1172
-
1173
- #### Basic Usage
1174
-
1175
- ```typescript
1176
- import { callCmd } from "~/app/cmds";
1177
-
1178
- // Simple command with typed arguments
1179
- await callCmd("pub", { dev: true });
1180
-
1181
- // Command with multiple arguments
1182
- await callCmd("check", {
1183
- directory: "src",
1184
- checks: "missing-deps,file-extensions",
1185
- strict: true,
1186
- json: false
1187
- });
1188
-
1189
- // Command with no arguments
1190
- await callCmd("update");
1191
-
1192
- // Generators with typed arguments
1193
- await callCmd("rempts", {
1194
- init: "new-cmd another-cmd",
1195
- overwrite: true,
1196
- outFile: "src/app/cmds.ts"
1197
- });
1198
- ```
1199
-
1200
- #### Advanced Usage
1201
-
1202
- ```typescript
1203
- import { getTypedCmd } from "~/app/cmds";
1204
-
1205
- // Get command instance for more control
1206
- const { command, run } = await getTypedCmd("magic");
1207
-
1208
- console.log(`Running: ${command.meta.name}`);
1209
- console.log(`Description: ${command.meta.description}`);
1210
-
1211
- await run({
1212
- targets: ["dist-npm", "dist-jsr"],
1213
- concurrency: 4,
1214
- stopOnError: true
1215
- });
1216
- ```
1217
-
1218
- #### TypeScript Benefits
1219
-
1220
- ##### 1. Command Name Autocomplete
1221
-
1222
- When you type `callCmd("`, TypeScript will show all available commands.
1223
-
1224
- ##### 2. Argument Intellisense
1225
-
1226
- When you type the arguments object, you get full autocomplete for:
1227
-
1228
- - Argument names
1229
- - Argument types
1230
- - Required vs optional fields
1231
-
1232
- ##### 3. Type Validation
1233
-
1234
- ```typescript
1235
- // Correct usage
1236
- await callCmd("create", {
1237
- mode: "files", // Only "template" | "files" allowed
1238
- multiple: true // boolean
1239
- });
1240
-
1241
- // ❌ TypeScript errors
1242
- await callCmd("create", {
1243
- mode: "invalid", // Error: not assignable to type
1244
- multiple: "yes" // Error: string not assignable to boolean
1245
- });
1246
- ```
1247
-
1248
- ##### 4. Required Field Checking
1249
-
1250
- ```typescript
1251
- // ✅ Required field provided
1252
- await callCmd("magic", {
1253
- targets: ["dist-npm"] // Required field
1254
- });
1255
-
1256
- // TypeScript error: missing required field 'targets'
1257
- await callCmd("magic", {
1258
- concurrency: 4
1259
- });
1260
- ```
1261
-
1262
- ### Maintaining the System
1263
-
1264
- #### Adding New Commands
1265
-
1266
- 1. Create your command in `src/app/<command-name>/cmd.ts` using `defineCommand` and `defineArgs`
1267
- 2. Run the generator: `dler rempts --overwrite`
1268
- 3. The `CommandArgsMap` interface in `src/app/cmds.ts` will be automatically updated
1269
-
1270
- #### Manual Updates
1271
-
1272
- The `CommandArgsMap` interface is auto-generated. If you need custom types, you can add manual type assertions (it is more recommended to edit your command file instead and regenerate the types):
1273
-
1274
- ```typescript
1275
- interface CommandArgsMap {
1276
- myCommand: {
1277
- // Use union types for specific values
1278
- mode: "development" | "production";
1279
-
1280
- // Use template literal types for patterns
1281
- version: `${number}.${number}.${number}`;
1282
-
1283
- // Use branded types for validation
1284
- port: number & { __brand: "Port" };
1285
- };
1286
- }
1287
- ```
1288
-
1289
- ### Migration from Old System
1290
-
1291
- #### Before (Old System, still supported)
1292
-
1293
- ```typescript
1294
- import { runCmd } from "@reliverse/rempts";
1295
- import { getPubCmd } from "./app/cmds";
1296
-
1297
- // No type safety, string-based arguments
1298
- await runCmd(await getPubCmd(), [`--dev=${isDev}`]);
1299
- ```
1300
-
1301
- ### After (New System)
1302
-
1303
- ```typescript
1304
- import { callCmd } from "./app/cmds";
1305
-
1306
- // Full type safety and intellisense
1307
- await callCmd("pub", { dev: isDev });
1308
- ```
1309
-
1310
- ### Implementation Details
1311
-
1312
- The system works by:
1313
-
1314
- 1. **Command Loading**: Commands are still loaded dynamically using `loadCommand()`
1315
- 2. **Argument Conversion**: Typed arguments are converted to string array format that `runCmd` expects
1316
- 3. **Type Mapping**: `CommandArgsMap` interface maps command names to their argument types
1317
- 4. **Generic Types**: `callCmd<T extends keyof CommandArgsMap>` provides type safety
1318
-
1319
- ### Generator Usage
1320
-
1321
- The typed command system also supports calling generators with full intellisense:
1322
-
1323
- #### Creating New Commands
1324
-
1325
- ```typescript
1326
- // Create new commands with typed arguments
1327
- await callCmd("rempts", {
1328
- init: "auth login logout", // Commands to create
1329
- overwrite: true, // Overwrite existing
1330
- outFile: "src/app/cmds.ts" // Export file path
1331
- });
1332
-
1333
- // Create commands in custom location
1334
- await callCmd("rempts", {
1335
- init: "api-handler",
1336
- customCmdsRoot: "src/modules/api",
1337
- outFile: "src/modules/api/exports.ts",
1338
- overwrite: true
1339
- });
1340
- ```
1341
-
1342
- #### Regenerating Exports
1343
-
1344
- ```typescript
1345
- // Regenerate exports file only
1346
- await callCmd("rempts", {
1347
- overwrite: true,
1348
- outFile: "src/app/cmds.ts"
1349
- });
1350
-
1351
- // Generate exports for specific directories
1352
- await callCmd("rempts", {
1353
- cmdDirs: ["build", "pub", "magic"],
1354
- outFile: "src/app/core-cmds.ts",
1355
- overwrite: true
1356
- });
1357
- ```
1358
-
1359
- #### Batch Operations
1360
-
1361
- ```typescript
1362
- // Create multiple commands programmatically
1363
- const modules = ["auth", "db", "api", "deploy"];
1364
-
1365
- for (const module of modules) {
1366
- await callCmd("rempts", {
1367
- init: `${module}-create ${module}-update ${module}-delete`,
1368
- customCmdsRoot: `src/modules/${module}`,
1369
- outFile: `src/modules/${module}/cmds.ts`,
1370
- overwrite: true
1371
- });
1372
- }
1373
- ```
1374
-
1375
- ## Contributing
1376
-
1377
- Bug report? Prompt idea? Want to build the best DX possible?
1378
-
1379
- You're in the right place! Please help us make the best CLI toolkit possible.
1380
-
1381
- ### Notices For Contributors
1382
-
1383
- **TypeScript Support**:
1384
-
1385
- All APIs are fully typed. See [`src/types.ts`](./src/types.ts) for advanced customization and type inference.
1386
-
1387
- **Examples**:
1388
-
1389
- - **Classic CLI:** [`example/launcher/classic.ts`](./example/launcher/classic.ts)
1390
- - **Modern Minimal CLI:** [`example/launcher/modern.ts`](./example/launcher/modern.ts)
1391
- - **Full Prompt Demo:** [`example/prompts/mod.ts`](./example/prompts/mod.ts)
1392
-
1393
- **Components and Utilities**:
1394
-
1395
- - **components/**: All prompt UIs, CLI output, launcher logic, etc.
1396
- - **utils/**: Color, error, validation, streaming, and system helpers.
1397
- - **hooks/**: Useful hooks for prompt state and effects.
1398
-
1399
- ### Helpful Links
1400
-
1401
- - [CLI application with the Node.js Readline module](https://dev.to/camptocamp-geo/cli-application-with-the-nodejs-readline-module-48ic)
1402
-
1403
- ## TODO
1404
-
1405
- - [ ] migrate to `dler libs` in the future (all main components will be published as separate packages; `@reliverse/rempts` will be a wrapper for all of them)
1406
- - [ ] migrate all tests to `bun:test`
1407
-
1408
- ## Related
1409
-
1410
- - [`@reliverse/rse`](https://npmjs.com/package/@reliverse/rse) – CLI-first toolkit for fullstack workflows
1411
- - [`@reliverse/relinka`](https://npmjs.com/package/@reliverse/relinka) Styled CLI logs, steps, and symbols
1412
-
1413
- ## Shoutouts
1414
-
1415
- - [citty](https://github.com/unjs/citty#readme) - launcher design inspiration
1416
-
1417
- ## Support
1418
-
1419
- Bug report? Prompt idea? Want to build the best DX possible?
1420
-
1421
- You're in the right place:
1422
-
1423
- - ✨ [Star the repo](https://github.com/reliverse/rempts)
1424
- - 💬 [Join the Discord](https://discord.gg/3GawfWfAPe)
1425
- - ❤️ [Sponsor @blefnk](https://github.com/sponsors/blefnk)
1426
-
1427
- > *No classes. No magic. Just clean, composable tools for CLI devs.*
1428
-
1429
- ## License
1430
-
1431
- 💖 MIT (see [LICENSE](./LICENSE) and [LICENCES](./LICENSES)) © [blefnk (Nazar Kornienko)](https://github.com/blefnk)
1
+ # 📃 rempts • powerful js/ts cli builder
2
+
3
+ [sponsor](https://github.com/sponsors/blefnk) — [discord](https://discord.gg/Pb8uKbwpsJ) — [repo](https://github.com/reliverse/dler) — [npm](https://npmjs.com/@reliverse/rempts)
4
+
5
+ > @reliverse/rempts is a modern, type-safe toolkit for building delightful cli experiences. it's fast, flexible, and made for developer happiness. file-based commands keep things simple—no clutter, just clean and easy workflows. this is how cli should feel.
6
+
7
+ ## Features
8
+
9
+ - 😘 drop-in to libraries like `unjs/citty` and `@clack/prompts`
10
+ - 📝 includes comprehensive set of built-in cli prompts
11
+ - 📂 file-based commands (app-router style by default)
12
+ - 🫂 rempts keeps you from fighting with your CLI tool
13
+ - 🏎️ prompt engine that *feels* modern — and actually is
14
+ - ✨ rempts is your end-to-end CLI UI + command framework
15
+ - 🌿 multi-level file-based subcommands (sibling + nested)
16
+ - 💪 built for DX precision and high-context terminal UX
17
+ - 🎭 looks great in plain scripts or full CLI apps
18
+ - 🎨 customizable themes and styled output
19
+ - 📦 built-in output formatter and logger
20
+ - 🚨 crash-safe (Ctrl+C, SIGINT, errors)
21
+ - ⚡ blazing-fast, zero runtime baggage
22
+ - 🧩 router + argument parser built-in
23
+ - 🧠 type-safe from args to prompts
24
+ - 📐 smart layout for small terminals
25
+ - 🎛️ override styles via prompt options
26
+ - 🪄 minimal API surface, maximum expressiveness
27
+ - 🧪 scriptable for testing, stable for production
28
+ - 🏞️ no more hacking together `inquirer`/`citty`/`commander`/`chalk`
29
+ - 🆕 automatic command creation (`bun dler rempts --init cmd1 cmd2`)
30
+ - 🐦‍🔥 automatic creation of `src/app/cmds.ts` file (`bun dler rempts`)
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ bun add @reliverse/rempts
36
+ ```
37
+
38
+ ## Usage Examples
39
+
40
+ - [Prompts](#prompts)
41
+ - [Launcher](#launcher)
42
+
43
+ ## Screenshot
44
+
45
+ ![rempts Example CLI Screenshot](./example/example.png)
46
+
47
+ ## API Overview
48
+
49
+ All main prompts APIs are available from the package root:
50
+
51
+ ```ts
52
+ import {
53
+ // ...prompts
54
+ inputPrompt, selectPrompt, multiselectPrompt, numberPrompt,
55
+ confirmPrompt, togglePrompt,
56
+ startPrompt, endPrompt, resultPrompt, nextStepsPrompt,
57
+ // ...hooks
58
+ createSpinner,
59
+ // ...launcher
60
+ createCli, defineCommand, defineArgs,
61
+ // ...types
62
+ // ...more
63
+ } from "@reliverse/rempts";
64
+ ```
65
+
66
+ > See [`src/mod.ts`](./src/mod.ts) for the full list of exports.
67
+
68
+ ## Prompts
69
+
70
+ ### Built-in Prompts
71
+
72
+ | Prompt | Description |
73
+ |---------------------------|-----------------------------------------------------------|
74
+ | `createSpinner` | Start/stop spinner |
75
+ | `inputPrompt` | Single-line input (with mask support, e.g. for passwords) |
76
+ | `selectPrompt` | Single-choice radio menu |
77
+ | `multiselectPrompt` | Multi-choice checkbox menu |
78
+ | `numberPrompt` | Type-safe number input |
79
+ | `confirmPrompt` | Yes/No toggle |
80
+ | `togglePrompt` | Custom on/off toggles |
81
+ | `resultPrompt` | Show results in a styled box |
82
+ | `nextStepsPrompt` | Show next steps in a styled list |
83
+ | `startPrompt`/`endPrompt` | Makes CLI start/end flows look nice |
84
+ | `datePrompt` | Date input with format validation |
85
+ | `anykeyPrompt` | Wait for any keypress |
86
+
87
+ ### Aliases
88
+
89
+ To help you migrate from the different CLI frameworks, `@reliverse/rempts` has some aliases for the most popular prompts.
90
+
91
+ | Prompt | Aliases |
92
+ |-----------------------|------------------|
93
+ | `createCli` | `runMain` |
94
+ | `onCmdInit` | `setup` |
95
+ | `onCmdExit` | `cleanup` |
96
+ | `createSpinner` | `spinner` |
97
+ | `selectPrompt` | `select` |
98
+ | `multiselectPrompt` | `multiselect` |
99
+ | `inputPrompt` | `text`, `input` |
100
+ | `confirmPrompt` | `confirm` |
101
+ | `introPrompt` | `intro`, `start` |
102
+ | `outroPrompt` | `outro`, `end` |
103
+ | `log` | `relinka` |
104
+
105
+ ### Prompts Usage Example
106
+
107
+ ```ts
108
+ import { relinka } from "@reliverse/relinka";
109
+
110
+ import {
111
+ startPrompt,
112
+ inputPrompt,
113
+ selectPrompt,
114
+ defineCommand,
115
+ runMain
116
+ } from "@reliverse/rempts";
117
+
118
+ async function main() {
119
+ await startPrompt({ title: "Project Setup" });
120
+
121
+ const name = await inputPrompt({
122
+ message: "What's your project name?",
123
+ initialValue: "my-cool-project", // Pre-fills the input field
124
+ defaultValue: "untitled-project", // Used if user submits empty input
125
+ });
126
+
127
+ const spinner = createSpinner({
128
+ text: "Loading...",
129
+ indicator: "timer", // or "dots"
130
+ frames: ["◒", "◐", "◓", "◑"], // custom frames
131
+ delay: 80, // custom delay
132
+ onCancel: () => {
133
+ console.log("Operation cancelled");
134
+ },
135
+ cancelMessage: "Operation cancelled by user",
136
+ errorMessage: "Operation failed",
137
+ signal: abortController.signal,
138
+ }).start();
139
+
140
+ // The spinner will show:
141
+ // ◒ Loading... [5s]
142
+ // With animated frames and timer
143
+
144
+ const framework = await selectPrompt({
145
+ message: "Pick your framework",
146
+ options: [
147
+ { value: "next", label: "Next.js" },
148
+ { value: "svelte", label: "SvelteKit" },
149
+ { value: "start", label: "TanStack Start" },
150
+ ],
151
+ });
152
+
153
+ console.log("Your result:", { name, framework });
154
+ };
155
+
156
+ await main();
157
+ ```
158
+
159
+ **Available spinner options:**
160
+
161
+ | Option | Description |
162
+ |--------|-------------|
163
+ | `cancelMessage` | The message to display when the spinner is cancelled |
164
+ | `color` | The color of the spinner |
165
+ | `delay` | The delay between frames |
166
+ | `errorMessage` | The message to display when the spinner fails |
167
+ | `failText` | The text to display when the spinner fails |
168
+ | `frames` | The frames to use for the spinner |
169
+ | `hideCursor` | Whether to hide the cursor |
170
+ | `indicator` | The indicator to use for the spinner |
171
+ | `onCancel` | The function to call when the spinner is cancelled |
172
+ | `prefixText` | The text to display before the spinner |
173
+ | `signal` | The signal to use for the spinner |
174
+ | `silent` | Whether to hide the spinner |
175
+ | `spinner` | The spinner to use for the spinner |
176
+ | `successText` | The text to display when the spinner succeeds |
177
+ | `text` | The text to display next to the spinner |
178
+
179
+ **Available indicator options:**
180
+
181
+ | Option | Description |
182
+ |--------|-------------|
183
+ | `timer` | The timer indicator |
184
+ | `dots` | The dots indicator |
185
+
186
+ **Available signal options:**
187
+
188
+ | Option | Description |
189
+ |--------|-------------|
190
+ | `abortController.signal` | The signal to use for the spinner |
191
+
192
+ **Available frames options:**
193
+
194
+ | Option | Description |
195
+ |--------|-------------|
196
+ | `["◒", "◐", "◓", "◑"]` | The frames to use for the spinner |
197
+
198
+ **Available delay options:**
199
+
200
+ | Option | Description |
201
+ |--------|-------------|
202
+ | `80` | The delay between frames |
203
+
204
+ **Available onCancel options:**
205
+
206
+ | Option | Description |
207
+ |--------|-------------|
208
+ | `() => { console.log("Operation cancelled"); }` | The function to call when the spinner is cancelled |
209
+
210
+ **Available inputPrompt options:**
211
+
212
+ | Option | Type | Description |
213
+ |--------|------|-------------|
214
+ | `message` | `string` | The prompt message (required) |
215
+ | `title` | `string` | Optional title. When both `title` and `message` are provided, title is shown first, then message (dimmed). When only `message` is provided, it acts as the title. |
216
+ | `charLimit` | `number` | Maximum character limit for input |
217
+ | `required` | `boolean` | Whether input is required (default: `true`). When `false`, cancelling the prompt returns an empty string instead of throwing `PromptCancelledError`. |
218
+ | `echoMode` | `"normal" \| "password" \| "none"` | Input echo mode (default: `"normal"`) |
219
+ | `validateOkPrefix` | `string` | Prefix to show when validation passes |
220
+ | `validateErrPrefix` | `string` | Prefix to show when validation fails |
221
+ | `defaultValue` | `string` | The value that is used if the user just presses Enter without typing anything. For input prompts, this is returned when the input field is empty. |
222
+ | `initialValue` | `string` | The value that is pre-filled in the input field when the prompt appears (user can edit it). This is different from `defaultValue` - `initialValue` is what the user sees and can modify, while `defaultValue` is what gets returned if they submit an empty input. |
223
+ | `validate` | `(value: string) => boolean \| string \| null \| undefined` | Custom validation function. Returns `true`, `null`, or `undefined` for valid input. Returns `false` or a string (error message) for invalid input. If validation fails, the prompt will re-prompt with the error message. |
224
+
225
+ > `inputPrompt` returns `Promise<string>`. When `required` is `true` (default), cancellation throws `PromptCancelledError`. When `required` is `false`, cancellation returns an empty string.
226
+
227
+ **Available selectPrompt options:**
228
+
229
+ | Option | Type | Description |
230
+ |--------|------|-------------|
231
+ | `message` | `string` | The prompt message (required) |
232
+ | `title` | `string` | Optional title. When both `title` and `message` are provided, title is shown first, then message (dimmed). When only `message` is provided, it acts as the title. |
233
+ | `options` | `readonly SelectionItem[]` | List of items with `value`, `label`, optional `hint`, and optional `disabled` |
234
+ | `perPage` | `number` | How many options to show per page (default: `5`) |
235
+ | `headerText` | `string` | Optional header text (defaults to formatted title/message) |
236
+ | `footerText` | `string` | Optional footer hint |
237
+ | `required` | `boolean` | When `false`, cancelling the prompt resolves to `null` instead of throwing |
238
+ | `autocomplete` | `boolean` | When `true` (default), users can type to jump between matching options |
239
+ | `defaultValue` | `string` | The value that is selected by default (if user presses Enter without changing selection). |
240
+ | `initialValue` | `string` | The value that the cursor starts on (user can navigate away). |
241
+
242
+ > `selectPrompt` has typed overloads: when `required` is omitted or `true`, it resolves to the selected value; when `required` is `false`, it resolves to either the selected value or `null`.
243
+
244
+ **Available multiselectPrompt options:**
245
+
246
+ | Option | Type | Description |
247
+ |--------|------|-------------|
248
+ | `message` | `string` | The prompt message (required) |
249
+ | `title` | `string` | Optional title. When both `title` and `message` are provided, title is shown first, then message (dimmed). When only `message` is provided, it acts as the title. |
250
+ | `options` | `readonly SelectionItem[]` | List of items with `value`, `label`, optional `hint`, and optional `disabled` |
251
+ | `perPage` | `number` | How many options to show per page (default: `5`) |
252
+ | `headerText` | `string` | Optional header text (defaults to formatted title/message) |
253
+ | `footerText` | `string` | Optional footer hint (defaults to usage instructions) |
254
+ | `required` | `boolean` | When `false`, cancelling the prompt resolves to `null`; otherwise a cancellation throws `PromptCancelledError` |
255
+ | `autocomplete` | `boolean` | When `true` (default), typing filters/highlights matching options |
256
+ | `defaultValue` | `string[]` | Array of values to pre-select (pre-checked items that user can unselect). If both `defaultValue` and `initialValue` are specified, `initialValue` is preferred. |
257
+ | `initialValue` | `string[]` | Array of values to pre-select (pre-checked items that user can unselect). Preferred over `defaultValue` if both are specified. The cursor starts on the first preselected value. |
258
+
259
+ > The resolved value is always an array of the selected option values. When `required` is `false`, the promise can resolve to `null` if the user cancels.
260
+
261
+ **Available confirmPrompt options:**
262
+
263
+ | Option | Type | Description |
264
+ |--------|------|-------------|
265
+ | `message` | `string` | The prompt message (required) |
266
+ | `title` | `string` | Optional title. When both `title` and `message` are provided, title is shown first, then message (dimmed). When only `message` is provided, it acts as the title. |
267
+ | `headerText` | `string` | Optional header text (defaults to formatted title/message) |
268
+ | `footerText` | `string` | Optional footer hint (defaults to navigation instructions) |
269
+ | `required` | `boolean` | When `false`, cancelling returns `defaultValue` or `false` instead of throwing; when `true` (default), cancelling throws `PromptCancelledError` |
270
+ | `defaultValue` | `boolean` | The value that is selected by default (if user presses Enter without changing selection). |
271
+ | `initialValue` | `boolean` | The value that the cursor starts on (user can navigate away). |
272
+
273
+ > `confirmPrompt` returns `Promise<boolean>`. When `required` is `true` (default), cancellation throws `PromptCancelledError`. When `required` is `false`, cancellation returns `defaultValue` or `false`.
274
+
275
+ **inputPrompt validation examples:**
276
+
277
+ ```ts
278
+ // Simple boolean validation
279
+ const email = await inputPrompt({
280
+ message: "Enter your email:",
281
+ validate: (value) => {
282
+ if (!value.includes("@")) {
283
+ return "Please enter a valid email address";
284
+ }
285
+ return true; // or return null/undefined
286
+ },
287
+ });
288
+
289
+ // Regex validation
290
+ const username = await inputPrompt({
291
+ message: "Enter username:",
292
+ validate: (value) => {
293
+ if (!/^[a-z0-9_]+$/.test(value)) {
294
+ return "Username must contain only lowercase letters, numbers, and underscores";
295
+ }
296
+ return true;
297
+ },
298
+ });
299
+
300
+ // Multiple validation rules
301
+ const password = await inputPrompt({
302
+ message: "Enter password:",
303
+ echoMode: "password",
304
+ validate: (value) => {
305
+ if (value.length < 8) {
306
+ return "Password must be at least 8 characters";
307
+ }
308
+ if (!/[A-Z]/.test(value)) {
309
+ return "Password must contain at least one uppercase letter";
310
+ }
311
+ if (!/[0-9]/.test(value)) {
312
+ return "Password must contain at least one number";
313
+ }
314
+ return true;
315
+ },
316
+ });
317
+ ```
318
+
319
+ ## Launcher
320
+
321
+ > **Note**: `runMain` is now an alias for `createCli` and is still supported for backward compatibility. The new `createCli` API provides a more intuitive object-based configuration format.
322
+
323
+ ### Automatic command creation
324
+
325
+ ```bash
326
+ bun add -D @reliverse/dler
327
+ bun dler rempts --init cmd1 cmd2 # creates `src/app/cmd1/cmd.ts` and `src/app/cmd2/cmd.ts` files
328
+ bun dler rempts # creates `src/app/cmds.ts` file
329
+ ```
330
+
331
+ ### Terminology
332
+
333
+ - **Launcher/Router**: The main entry point for your CLI. Visit [CLI Launcher (Router)](#cli-launcher-router) section to learn more.
334
+ - **Command**: A command is a function that defines the inner script launched by the main script where runMain() is used or by some other command.
335
+ - **Argument**: An argument is a value that is passed to a command.
336
+ - **Flag**: A flag is a boolean argument that is used to enable or disable a feature.
337
+ - **Option**: An option is a named argument that is used to configure a command.
338
+
339
+ #### Launcher Usage Example
340
+
341
+ **Important**: Ensure your commands don't have `await main();`, `await createCli();`, or something like that — to prevent any unexpected behavior. Only main command should have it.
342
+
343
+ ```ts
344
+ import { relinka } from "@reliverse/relinka";
345
+
346
+ import { defineCommand, createCli } from "@reliverse/rempts";
347
+
348
+ const main = defineCommand({
349
+ meta: {
350
+ name: "rempts",
351
+ version: "1.0.0",
352
+ description: "rempts Launcher Playground CLI",
353
+ },
354
+ onCmdInit() {
355
+ relinka("success", "Setup");
356
+ },
357
+ onCmdExit() {
358
+ relinka("success", "Cleanup");
359
+ },
360
+ commands: {
361
+ build: () => import("./app/build/cmd.js").then((r) => r.default),
362
+ deploy: () => import("./app/deploy/cmd.js").then((r) => r.default),
363
+ debug: () => import("./app/debug/cmd.js").then((r) => r.default),
364
+ },
365
+ });
366
+
367
+ // New object format (recommended)
368
+ await createCli({
369
+ mainCommand: main,
370
+ fileBased: {
371
+ enable: true,
372
+ cmdsRootPath: "my-cmds", // default is `./app`
373
+ },
374
+ // Optionally disable auto-exit to handle errors manually:
375
+ autoExit: false,
376
+ });
377
+
378
+ // Legacy format (still supported)
379
+ await createCli(main, {
380
+ fileBased: {
381
+ enable: true,
382
+ cmdsRootPath: "my-cmds", // default is `./app`
383
+ },
384
+ // Optionally disable auto-exit to handle errors manually:
385
+ autoExit: false,
386
+ });
387
+ ```
388
+
389
+ This flexibility allows you to easily build a rich, multi-command CLI with minimal boilerplate. The launcher even supports nested commands, making it simple to construct complex CLI applications.
390
+
391
+ #### File-Based Commands
392
+
393
+ Drop a `./src/cli/app/add/index.ts` and it's live.
394
+
395
+ ```ts
396
+ import { defineArgs, defineCommand } from "@reliverse/rempts";
397
+ export default defineCommand({
398
+ meta: {
399
+ name: "add",
400
+ version: "1.0.0",
401
+ description: "Add stuff to your project",
402
+ },
403
+ args: {
404
+ name: defineArgs({ // 💡 PRO TIP: use defineArgs() to get fully correct intellisense
405
+ type: "string",
406
+ required: true,
407
+ description: "Name of what to add",
408
+ }),
409
+ },
410
+ async run({ args }) {
411
+ relinka("log", "Adding:", args.name);
412
+ },
413
+ });
414
+ ```
415
+
416
+ **Supports**:
417
+
418
+ - `arg-cmdName.{ts,js}`,
419
+ - `cmdName/index.{ts,js}`,
420
+ - `cmdName/cmdName-mod.{ts,js}`,
421
+ - **Multi-level subcommands:** `foo/bar/baz/cmd.ts` → `my-cli foo bar baz`
422
+ - And more — with automatic usage output.
423
+
424
+ **Hint**:
425
+
426
+ - Install `bun add -D @reliverse/dler`
427
+ - Use `bun dler rempts --init cmd1 cmd2` to init commands for rempts launcher's automatically
428
+
429
+ ### Advanced Launcher Usage
430
+
431
+ ```ts
432
+ defineCommand({
433
+ meta: { name: "cli", version: "1.0.0" },
434
+ args: {
435
+ name: { type: "string", required: true },
436
+ verbose: { type: "boolean", default: false },
437
+ animals: { type: "array", default: ["cat","dog"] },
438
+ },
439
+ async run({ args, raw }) { // or `async run(ctx)`
440
+ relinka("log", args.name, args.verbose, args.animals); // or `relinka("log", ctx.args.name, ...);`
441
+ },
442
+ });
443
+ ```
444
+
445
+ **Supports**:
446
+
447
+ - `positional` args
448
+ - `array` types (`--tag foo --tag bar`)
449
+ - Default values, validations, descriptions
450
+ - Full help rendering from metadata
451
+
452
+ **By the way! Multi-level subcommands!**
453
+
454
+ You can also nest subcommands arbitrarily deep:
455
+
456
+ ```bash
457
+ app/
458
+ foo/
459
+ bar/
460
+ baz/
461
+ cmd.ts
462
+ ```
463
+
464
+ Invoke with:
465
+
466
+ ```bash
467
+ my-cli foo bar baz --some-flag
468
+ ```
469
+
470
+ The launcher will recursively traverse subfolders for each non-flag argument, loading the deepest `cmd.ts`/`cmd.js` it finds, and passing the remaining arguments to it.
471
+
472
+ See [example/launcher/app/nested](./example/launcher/app/nested/) and [example/launcher/app/sibling](./example/launcher/app/sibling/) folders to learn more.
473
+
474
+ When playing with the example, you can run e.g. `bun dev:modern nested foo bar baz` to see the result in action.
475
+
476
+ ### Playground
477
+
478
+ ```bash
479
+ git clone https://github.com/reliverse/dler
480
+ cd rempts
481
+ bun i
482
+ bun dev
483
+ ```
484
+
485
+ - `bun dev:prompts`: This example will show you a `multiselectPrompt()` where you can choose which CLI prompts you want to play with.
486
+ - `bun dev:modern`: This example will show you a modern CLI launcher usage with file-based commands.
487
+ - `bun dev:classic`: This example will show you a classic CLI launcher usage with programmatic commands.
488
+
489
+ ### Launcher Usage Examples
490
+
491
+ #### Minimal Usage Example
492
+
493
+ **1 Create a `src/mod.ts` file:**
494
+
495
+ ```ts
496
+ import { createCli, defineCommand } from "@reliverse/rempts";
497
+
498
+ // New object format (recommended)
499
+ await createCli({
500
+ mainCommand: defineCommand({}),
501
+ });
502
+
503
+ // Legacy format (still supported)
504
+ await createCli(defineCommand({}));
505
+ ```
506
+
507
+ **2 Run the following:**
508
+
509
+ ```bash
510
+ bun add -D @reliverse/dler
511
+ bun dler rempts --init my-cmd-1 # or: dler rempts --init my-cmd-1 my-cmd-2 --main src/mod.ts
512
+ # * `--main` is optional, default is `./src/mod.ts`
513
+ # * you can specify multiple commands at once
514
+ ```
515
+
516
+ **3 Visit `src/app/my-cmd-1/mod.ts` and edit it:**
517
+
518
+ ```ts
519
+ export default defineCommand({
520
+ run() { console.log("Hello, world!"); },
521
+ });
522
+ ```
523
+
524
+ **4. Test it:**
525
+
526
+ ```bash
527
+ bun src/mod.ts
528
+ ```
529
+
530
+ #### Medium Usage Example
531
+
532
+ ```ts
533
+ import { defineCommand, createCli } from "@reliverse/rempts";
534
+
535
+ const main = defineCommand({
536
+ meta: {
537
+ name: "mycli",
538
+ },
539
+ run() {
540
+ console.log("Happy, Reliversing!");
541
+ },
542
+ });
543
+
544
+ // New object format (recommended)
545
+ await createCli({
546
+ mainCommand: main,
547
+ });
548
+
549
+ // Legacy format (still supported)
550
+ await createCli(main);
551
+ ```
552
+
553
+ #### Classic Usage Example
554
+
555
+ ```ts
556
+ import { relinka } from "@reliverse/relinka";
557
+
558
+ import {
559
+ startPrompt,
560
+ inputPrompt,
561
+ selectPrompt,
562
+ defineCommand,
563
+ createCli
564
+ } from "@reliverse/rempts";
565
+
566
+ const main = defineCommand({
567
+ meta: {
568
+ name: "mycli",
569
+ version: "1.0.0",
570
+ description: "CLI powered by rempts",
571
+ },
572
+ args: {
573
+ name: {
574
+ type: "string",
575
+ required: true,
576
+ description: "The name of the project",
577
+ },
578
+ },
579
+ async run({ args }) {
580
+ await startPrompt({
581
+ title: "Project Setup",
582
+ });
583
+
584
+ const name = await inputPrompt({
585
+ message: "What's your project name?",
586
+ defaultValue: args.name,
587
+ });
588
+
589
+ const framework = await selectPrompt({
590
+ message: "Pick your framework",
591
+ options: [
592
+ { value: "next", label: "Next.js" },
593
+ { value: "svelte", label: "SvelteKit" },
594
+ { value: "start", label: "TanStack Start" },
595
+ ],
596
+ });
597
+
598
+ relinka("log", "You have selected:", { name, framework });
599
+ },
600
+ });
601
+
602
+ // New object format (recommended)
603
+ await createCli({
604
+ mainCommand: main,
605
+ });
606
+
607
+ // Legacy format (still supported)
608
+ await createCli(main);
609
+ ```
610
+
611
+ #### Advanced Usage Example
612
+
613
+ ```ts
614
+ import { relinka } from "@reliverse/relinka";
615
+
616
+ import {
617
+ startPrompt,
618
+ inputPrompt,
619
+ selectPrompt,
620
+ defineCommand,
621
+ runMain,
622
+ } from "@reliverse/rempts";
623
+
624
+ /**
625
+ * Main command defined using `defineCommand()`.
626
+ *
627
+ * This command demonstrates the full range of launcher features along with all supported argument types:
628
+ *
629
+ * - Global Usage Handling: Automatically processes `--help` and `--version`.
630
+ * - File-Based Commands: Scans "app" for commands (e.g., `init`).
631
+ * - Comprehensive Argument Parsing: Supports positional, boolean, string, number, and array arguments.
632
+ * - Interactive Prompts: Uses built-in prompt functions for an engaging CLI experience.
633
+ */
634
+ const mainCommand = defineCommand({
635
+ meta: {
636
+ name: "rempts",
637
+ version: "1.6.0",
638
+ description:
639
+ "An example CLI that supports file-based commands and all argument types.",
640
+ },
641
+ args: {
642
+ // Positional arguments
643
+ inputFile: {
644
+ type: "positional",
645
+ description: "Path to the input file (only for the main command).",
646
+ },
647
+ config: {
648
+ type: "positional",
649
+ description: "Path to the configuration file.",
650
+ },
651
+ // Boolean arguments
652
+ verbose: {
653
+ type: "boolean",
654
+ default: false,
655
+ description: "Whether to print verbose logs in the main command.",
656
+ },
657
+ debug: {
658
+ type: "boolean",
659
+ default: false,
660
+ description: "Enable debug mode for additional logging.",
661
+ },
662
+ // String argument
663
+ name: {
664
+ type: "string",
665
+ description: "The name of the project.",
666
+ },
667
+ // Number argument
668
+ timeout: {
669
+ type: "number",
670
+ default: 30,
671
+ description: "Timeout in seconds for the CLI operation.",
672
+ },
673
+ // Array argument
674
+ tags: {
675
+ type: "array",
676
+ default: ["cli", "rempts"],
677
+ description: "List of tags associated with the project.",
678
+ },
679
+ },
680
+ async run({ args, raw }) {
681
+ // Display invocation details and parsed arguments.
682
+ relinka("log", "Main command was invoked!");
683
+ relinka("log", "Parsed main-command args:", args);
684
+ relinka("log", "Raw argv:", raw);
685
+ relinka("log", "\nHelp: `rempts --help`, `rempts cmdName --help`");
686
+
687
+ // Begin interactive session with a prompt.
688
+ await startPrompt({
689
+ title: "Project Setup",
690
+ });
691
+
692
+ // Ask for the project name, falling back to provided argument or a default.
693
+ const projectName = await inputPrompt({
694
+ message: "What's your project name?",
695
+ defaultValue: args.name ?? "my-cool-cli",
696
+ });
697
+
698
+ // Let the user pick a framework from a select prompt.
699
+ const framework = await selectPrompt({
700
+ message: "Pick your framework",
701
+ options: [
702
+ { value: "next", label: "Next.js" },
703
+ { value: "svelte", label: "SvelteKit" },
704
+ { value: "start", label: "TanStack Start" },
705
+ ],
706
+ });
707
+
708
+ // Log all gathered input details.
709
+ relinka("log", "You have selected:", {
710
+ projectName,
711
+ framework,
712
+ inputFile: args.inputFile,
713
+ config: args.config,
714
+ verbose: args.verbose,
715
+ debug: args.debug,
716
+ timeout: args.timeout,
717
+ tags: args.tags,
718
+ });
719
+ },
720
+ });
721
+
722
+ /**
723
+ * The `createCli()` function sets up the launcher with several advanced features:
724
+ *
725
+ * - File-Based Commands: Enables scanning for commands within the "app" directory.
726
+ * - Alias Mapping: Shorthand flags (e.g., `-v`) are mapped to their full names (e.g., `--verbose`).
727
+ * - Strict Mode & Unknown Flag Warnings: Unknown flags are either warned about or handled via a callback.
728
+ * - Negated Boolean Support: Allows flags to be negated (e.g., `--no-verbose`).
729
+ * - Custom Unknown Flag Handler: Provides custom handling for unrecognized flags.
730
+ */
731
+ // New object format (recommended)
732
+ await createCli({
733
+ mainCommand: mainCommand,
734
+ fileBased: {
735
+ enable: true, // Enables file-based command detection.
736
+ cmdsRootPath: "app", // Directory to scan for commands.
737
+ },
738
+ alias: {
739
+ v: "verbose", // Maps shorthand flag -v to --verbose.
740
+ },
741
+ strict: false, // Do not throw errors for unknown flags.
742
+ warnOnUnknown: false, // Warn when encountering unknown flags.
743
+ negatedBoolean: true, // Support for negated booleans (e.g., --no-verbose).
744
+ // unknown: (flagName) => {
745
+ // relinka("warn", "Unknown flag encountered:", flagName);
746
+ // return false;
747
+ // },
748
+ });
749
+
750
+ // Legacy format (still supported)
751
+ await createCli(mainCommand, {
752
+ fileBased: {
753
+ enable: true, // Enables file-based command detection.
754
+ cmdsRootPath: "app", // Directory to scan for commands.
755
+ },
756
+ alias: {
757
+ v: "verbose", // Maps shorthand flag -v to --verbose.
758
+ },
759
+ strict: false, // Do not throw errors for unknown flags.
760
+ warnOnUnknown: false, // Warn when encountering unknown flags.
761
+ negatedBoolean: true, // Support for negated booleans (e.g., --no-verbose).
762
+ // unknown: (flagName) => {
763
+ // relinka("warn", "Unknown flag encountered:", flagName);
764
+ // return false;
765
+ // },
766
+ });
767
+ ```
768
+
769
+ ### CLI Launcher (Router)
770
+
771
+ Finally, a full-featured CLI launcher without the ceremony. `@reliverse/rempts`'s so called "launcher" is a uniquely powerful and ergonomic CLI toolkit—one that helps you build delightful developer experiences with less code and more confidence. The launcher supports both programmatically defined commands and file-based routing, so you can structure your CLI however you like. It automatically detects and loads commands from your filesystem and provides robust usage and error handling out-of-the-box. The launcher is more than just a command runner—it's a robust, developer-friendly engine with several advanced features and thoughtful design choices:
772
+
773
+ - **File-Based & Defined Commands:**
774
+ Use `commands` in your command definition or let the launcher automatically load commands from a specified directory.
775
+
776
+ - **Automatic Command Detection:**
777
+ The launcher scans your specified `cmdsRootPath` for command files matching common patterns such as:
778
+ - `arg-cmdName.{ts,js}`
779
+ - `cmdName/index.{ts,js}`
780
+ - `cmdName/cmdName-mod.{ts,js}`
781
+ - And more — with automatic usage output if a command file is not found.
782
+
783
+ - **Built-In Flag Handling:**
784
+ Automatically processes global flags such as:
785
+ - `--help` and `-h` to show usage details.
786
+ - `--version` and `-v` to display version information.
787
+ - `--debug` for verbose logging during development.
788
+
789
+ - **Unified Argument Parsing:**
790
+ Seamlessly combines positional and named arguments with zero configuration, auto-parsing booleans, strings, numbers, arrays, and even supporting negated flags like `--no-flag`.
791
+
792
+ - **Customizable Behavior:**
793
+ Options such as `fileBased.enable`, `cmdsRootPath`, and `autoExit` allow you to tailor the launcher's behavior. For example, you can choose whether the process should exit automatically on error or allow manual error handling.
794
+
795
+ - **Error Management & Usage Output:**
796
+ The launcher provides clear error messages for missing required arguments, invalid types, or command import issues, and it automatically displays usage information for your CLI.
797
+
798
+ - **Lifecycle Hooks:**
799
+ You can define optional lifecycle hooks in your main command:
800
+ - `onLauncherInit` and `onLauncherExit` (global, called once per CLI process)
801
+ - `onCmdInit` and `onCmdExit` (per-command, called before/after each command, but NOT for the main `run()` handler)
802
+
803
+ **Global Hooks:**
804
+ - `onLauncherInit`: Called once, before any command/run() is executed.
805
+ - `onLauncherExit`: Called once, after all command/run() logic is finished (even if an error occurs).
806
+
807
+ **Per-Command Hooks:**
808
+ - `onCmdInit`: Called before each command (not for main `run()`).
809
+ - `onCmdExit`: Called after each command (not for main `run()`).
810
+
811
+ This means:
812
+ - If your CLI has multiple commands, `onCmdInit` and `onCmdExit` will be called for each command invocation, not just once for the whole CLI process.
813
+ - If your main command has a `run()` handler (and no command is invoked), these hooks are **not** called; use the `run()` handler itself or the global hooks for such logic.
814
+ - This allows you to perform setup/teardown logic specific to each command execution.
815
+ - If you want logic to run only once for the entire CLI process, use `onLauncherInit` and `onLauncherExit`.
816
+
817
+ **Example:**
818
+
819
+ ```ts
820
+ const main = defineCommand({
821
+ onLauncherInit() { relinka('info', 'Global setup (once per process)'); },
822
+ onLauncherExit() { relinka('info', 'Global cleanup (once per process)'); },
823
+ onCmdInit() { relinka('info', 'Setup for each command'); },
824
+ onCmdExit() { relinka('info', 'Cleanup for each command'); },
825
+ commands: { ... },
826
+ run() { relinka('info', 'Main run handler (no command)'); },
827
+ });
828
+ // onLauncherInit/onLauncherExit are called once per process
829
+ // onCmdInit/onCmdExit are called for every command (not for main run())
830
+ // If you want per-run() logic, use the run() handler or global hooks
831
+ ```
832
+
833
+ - **Dynamic Usage Examples:**
834
+ - The launcher inspects your available commands and their argument definitions, then prints a plausible example CLI invocation for a random command directly in the help output. This helps users understand real-world usage at a glance.
835
+
836
+ - **File-Based & Programmatic Commands:**
837
+ - Both file-based and object commands are fully supported. The launcher can introspect their argument definitions and metadata for help, usage, and validation.
838
+ - File-based commands are auto-discovered from your filesystem, while programmatic commands can be defined inline in your main command.
839
+
840
+ - **Context-Aware Help Output:**
841
+ - The help/usage output adapts to your CLI's structure, showing available commands, their aliases, argument details, and even dynamic usage examples. It also displays global options and context-specific error messages.
842
+
843
+ - **Error Handling:**
844
+ - The launcher provides clear, actionable error messages for missing required arguments, invalid types, unknown commands, and import errors. It always shows relevant usage information to help users recover quickly.
845
+
846
+ - **Unified Argument Parsing:**
847
+ - All arguments (positional, named, boolean, string, number, array) are parsed and validated automatically. Negated flags (like `--no-flag`) are supported out of the box.
848
+
849
+ - **Extensible & Flexible:**
850
+ - The launcher is highly extensible. You can use it with both Bun and Node.js, and it works seamlessly with both file-based and programmatic command definitions. You can also customize its behavior with options like `autoExit`, `cmdsRootPath`, and more.
851
+
852
+ - **Bun & Node.js Support:**
853
+ - The launcher is designed to work in both Bun and Node.js environments, so you can use it in any modern JavaScript/TypeScript project.
854
+
855
+ - **Prompt-First, Modern UX:**
856
+ - The launcher integrates tightly with the prompt engine, so you can build interactive, delightful CLIs with minimal effort.
857
+
858
+ ### Launcher Programmatic Execution
859
+
860
+ For larger CLIs or when you want to programmatically run commands (e.g.: [prompt demo](./example/prompts/mod.ts), tests, etc), you can organize your commands in a `cmds.ts` file and use the `runCmd` utility. Example:
861
+
862
+ ```ts
863
+ // example/launcher/app/runcmd/cmd.ts
864
+
865
+ import { relinka } from "@reliverse/relinka";
866
+ import { defineArgs, defineCommand, runCmd } from "@reliverse/rempts";
867
+ import { cmdMinimal } from "../cmds";
868
+
869
+ export default defineCommand({
870
+ meta: {
871
+ name: "runcmd",
872
+ description:
873
+ "Demonstrate how to use runCmd() to invoke another command programmatically.",
874
+ },
875
+ args: defineArgs({
876
+ name: {
877
+ type: "string",
878
+ description: "your name",
879
+ },
880
+ }),
881
+ async run({ args }) {
882
+ // const username = args.name ?? "Alice";
883
+ const username = args.name; // intentionally missing fallback
884
+ relinka(
885
+ "info",
886
+ `Running the 'minimal' command using runCmd() with name='${username}'`,
887
+ );
888
+ await runCmd(await cmdMinimal(), ["--name", username]);
889
+ relinka("log", "Done running 'minimal' via runCmd().");
890
+ },
891
+ });
892
+ ```
893
+
894
+ ### Using `runCmd` with Flexible Argument Handling
895
+
896
+ The `runCmd` function supports flexible argument passing, automatically normalizing template literals and space-separated strings:
897
+
898
+ ```ts
899
+ import { runCmd } from "@reliverse/rempts";
900
+
901
+ // Traditional way - each argument as separate array element
902
+ await runCmd(cmd, ["--dev", "true", "--name", "John"]);
903
+
904
+ // Template literals work automatically
905
+ await runCmd(cmd, [`--dev ${isDev}`]); // Automatically converted to ["--dev", "true"]
906
+ await runCmd(cmd, [`--dev ${isDev} --build mod.ts`]); // ["--dev", "true", "--build", "mod.ts"]
907
+
908
+ // Mixed arrays with template literals and regular strings
909
+ await runCmd(cmd, [
910
+ `--dev ${isDev} --build mod.ts`,
911
+ "--pub true",
912
+ "--someBoolean",
913
+ ]);
914
+
915
+ // Multiple template literals
916
+ await runCmd(cmd, [`--dev ${isDev}`, `--name ${userName}`, `--count ${count}`]);
917
+ ```
918
+
919
+ **Remember**:
920
+
921
+ - If you need to pass a value with spaces (e.g. a name like "John Doe"), you should quote it in your template literal: `await runCmd(cmd, ['--name "John Doe"']);`
922
+ - Otherwise, it will be split into two arguments: `"John"` and `"Doe"`.
923
+ - We do not handle this intentionally, because some library users might rely on this Node.js behavior and handle it themselves in their own way (e.g. space can serve as a separator for values).
924
+
925
+ ### Loading Commands with `loadCommand`
926
+
927
+ The `loadCommand` utility helps you load command files from your filesystem. It automatically handles:
928
+
929
+ - Relative paths (both `./build` and `build` work the same)
930
+ - Automatic detection of `cmd.{ts,js}` files
931
+ - Clear error messages when files are not found
932
+
933
+ ```ts
934
+ import { loadCommand } from "@reliverse/rempts";
935
+
936
+ // These are equivalent:
937
+ const cmd1 = await loadCommand("./build"); // Looks for build/cmd.ts or build/cmd.js
938
+ const cmd2 = await loadCommand("build"); // Same as above
939
+ const cmd3 = await loadCommand("./build/cmd"); // Explicit path to cmd file
940
+
941
+ // You can then use the loaded command with runCmd:
942
+ await runCmd(cmd1, ["--some-flag"]);
943
+ ```
944
+
945
+ ```ts
946
+ // src/app/cmds.ts
947
+ export const getBuildCmd = async (): Promise<Command> => loadCommand("./build");
948
+
949
+ // src/cli.ts
950
+ import { runCmd } from "@reliverse/rempts";
951
+ import { getBuildCmd } from "./app/cmds";
952
+ await runCmd(await getBuildCmd(), ["--prod"]);
953
+ ```
954
+
955
+ **Error Handling:**
956
+ If the command file is not found, you'll get a clear error message:
957
+
958
+ ```bash
959
+ No command file found in /path/to/build. Expected to find either:
960
+ - /path/to/build/cmd.ts
961
+ - /path/to/build/cmd.js
962
+ Please ensure one of these files exists and exports a default command.
963
+ ```
964
+
965
+ **Best Practices:**
966
+
967
+ - Use `loadCommand` when you need to load commands from the filesystem
968
+ - Use `runCmd` to execute the loaded command with arguments
969
+ - Keep your command files in a consistent location (e.g., `src/app/yourCmdName/cmd.ts`)
970
+ - Export commands from a central file like `src/app/cmds.ts` for better organization
971
+
972
+ ```ts
973
+ // example/launcher/app/cmds.ts
974
+ import { loadCommand } from "@reliverse/rempts";
975
+
976
+ export async function getBuildCmd() {
977
+ return loadCommand("./build");
978
+ }
979
+
980
+ export async function getDeployCmd() {
981
+ return loadCommand("./deploy");
982
+ }
983
+
984
+ // Usage:
985
+ import { getBuildCmd } from "./cmds";
986
+ const buildCmd = await getBuildCmd();
987
+ await runCmd(buildCmd, ["--prod"]);
988
+ ```
989
+
990
+ ```ts
991
+ // example/launcher/app/minimal/cmd.ts
992
+
993
+ import { relinka } from "@reliverse/relinka";
994
+ import { defineArgs, defineCommand } from "@reliverse/rempts";
995
+
996
+ export default defineCommand({
997
+ meta: {
998
+ name: "minimal",
999
+ description: "hello world",
1000
+ },
1001
+ args: defineArgs({
1002
+ name: {
1003
+ type: "string",
1004
+ description: "your name",
1005
+ required: true,
1006
+ },
1007
+ }),
1008
+ run({ args }) {
1009
+ relinka("success", `👋 Hello, ${args.name}!`);
1010
+ },
1011
+ });
1012
+ ```
1013
+
1014
+ ### Using `runCmdWithSubcommands` for Subcommands and Nested Subcommands
1015
+
1016
+ If you need to programmatically run commands that support subcommands (including nested subcommands), use `runCmdWithSubcommands`:
1017
+
1018
+ ```ts
1019
+ import { runCmdWithSubcommands } from "@reliverse/rempts";
1020
+
1021
+ // Single-level subcommand
1022
+ await runCmdWithSubcommands(mainCmd, [`build --input src/mod.ts --someBoolean`]);
1023
+
1024
+ // Subcommand with positional arguments
1025
+ await runCmdWithSubcommands(mainCmd, [`build src/mod.ts --someBoolean`]);
1026
+
1027
+ // Nested subcommands
1028
+ await runCmdWithSubcommands(mainCmd, [`build someSubCmd src/mod.ts --no-cjs`]);
1029
+ await runCmdWithSubcommands(mainCmd, [`build sub1 sub2 sub3 file.ts --flag`]);
1030
+
1031
+ // Mixed array with subcommands
1032
+ await runCmdWithSubcommands(mainCmd, [
1033
+ `build someSubCmd src/mod.ts`,
1034
+ "--no-cjs",
1035
+ "--verbose"
1036
+ ]);
1037
+ ```
1038
+
1039
+ **Note:**
1040
+
1041
+ - `runCmdWithSubcommands` automatically normalizes template literals and space-separated strings, just like `runCmd`.
1042
+ - If you need to pass a value with spaces (e.g. a name like "John Doe"), you should quote it in your template literal: `await runCmdWithSubcommands(cmd, ['--name "John Doe"']);`
1043
+ - For subcommands, always use `runCmdWithSubcommands` for the most robust behavior.
1044
+
1045
+ ## Argument Types: Usage Comparison
1046
+
1047
+ Below is a demonstration of how to define and use all supported argument types in rempts: positional, boolean, string, number, and array. This includes example CLI invocations and the resulting parsed output.
1048
+
1049
+ ```ts
1050
+ import { defineCommand, createCli } from "@reliverse/rempts";
1051
+
1052
+ const main = defineCommand({
1053
+ meta: {
1054
+ name: "mycli",
1055
+ version: "1.0.0",
1056
+ description: "Demo of all argument types",
1057
+ },
1058
+ args: {
1059
+ // Positional argument (required)
1060
+ input: {
1061
+ type: "positional",
1062
+ required: true,
1063
+ description: "Input file path",
1064
+ },
1065
+ // Boolean flag (default: false)
1066
+ verbose: {
1067
+ type: "boolean",
1068
+ default: false,
1069
+ description: "Enable verbose output",
1070
+ },
1071
+ // String option (optional)
1072
+ name: {
1073
+ type: "string",
1074
+ description: "Your name",
1075
+ },
1076
+ // Number option (optional, with default)
1077
+ count: {
1078
+ type: "number",
1079
+ default: 1,
1080
+ description: "How many times to run",
1081
+ },
1082
+ // Array option (can be repeated, accepts any value)
1083
+ tags: {
1084
+ type: "array",
1085
+ default: ["demo"],
1086
+ description: "Tags for this run (repeatable)",
1087
+ },
1088
+ },
1089
+ run({ args }) {
1090
+ console.log("Parsed args:", args);
1091
+ },
1092
+ });
1093
+
1094
+ // New object format (recommended)
1095
+ await createCli({
1096
+ mainCommand: main,
1097
+ });
1098
+
1099
+ // Legacy format (still supported)
1100
+ await createCli(main);
1101
+ ```
1102
+
1103
+ ### Example CLI Invocations
1104
+
1105
+ #### 1. Positional argument
1106
+
1107
+ ```bash
1108
+ mycli input.txt
1109
+ # args.input = "input.txt"
1110
+ ```
1111
+
1112
+ #### 2. Boolean flag
1113
+
1114
+ ```bash
1115
+ mycli input.txt --verbose
1116
+ # args.verbose = true
1117
+ mycli input.txt --no-verbose
1118
+ # → args.verbose = false
1119
+ ```
1120
+
1121
+ #### 3. String option
1122
+
1123
+ ```bash
1124
+ mycli input.txt --name Alice
1125
+ # → args.name = "Alice"
1126
+ mycli input.txt
1127
+ # args.name = undefined
1128
+ ```
1129
+
1130
+ #### 4. Number option
1131
+
1132
+ ```bash
1133
+ mycli input.txt --count 5
1134
+ # args.count = 5
1135
+ mycli input.txt
1136
+ # → args.count = 1 (default)
1137
+ ```
1138
+
1139
+ #### 5. Array option (repeatable, accepts any value)
1140
+
1141
+ You can provide array values using any of the following syntaxes (mix and match as needed):
1142
+
1143
+ - Repeated flags:
1144
+
1145
+ ```bash
1146
+ mycli input.txt --tags foo --tags bar --tags baz
1147
+ # args.tags = ["foo", "bar", "baz"]
1148
+ ```
1149
+
1150
+ - Comma-separated values (with or without spaces):
1151
+
1152
+ ```bash
1153
+ mycli input.txt --tags foo,bar,baz
1154
+ mycli input.txt --tags foo, bar, baz
1155
+ # → args.tags = ["foo", "bar", "baz"]
1156
+ ```
1157
+
1158
+ - Bracketed values (must be passed as a single argument!):
1159
+
1160
+ ```bash
1161
+ mycli input.txt --tags "[foo,bar,baz]"
1162
+ # args.tags = ["foo", "bar", "baz"]
1163
+ ```
1164
+
1165
+ - Mix and match:
1166
+
1167
+ ```bash
1168
+ mycli input.txt --tags foo --tags "[bar,bar2,bar3]" --tags baz
1169
+ # args.tags = ["foo", "bar", "bar2", "bar3", "baz"]
1170
+ ```
1171
+
1172
+ > **Important:**
1173
+ >
1174
+ > - **Quoted values (single or double quotes around elements) are NOT supported and will throw an error.**
1175
+ > - Example: `--tags 'foo'` or `--tags "[\"bar\",'baz']"` will throw an error.
1176
+ > - **Bracketed or comma-separated lists must be passed as a single argument.**
1177
+ > - Example: `--tags "[foo,bar]"` (quotes around the whole value, not around elements)
1178
+ > - If you split a bracketed value across arguments, you will get a warning or incorrect parsing.
1179
+ > - **Shells remove quotes before passing arguments to the CLI.** If you want to pass a value with commas or brackets, always quote the whole value.
1180
+ > - **Troubleshooting:**
1181
+ > - If you see a warning about possible shell splitting, try quoting the whole value: `--tags "[a,b,c]"`
1182
+ > - If you see an error about quoted values, remove quotes around individual elements.
1183
+
1184
+ **Example error:**
1185
+
1186
+ ```bash
1187
+ $ bun example/launcher/modern.ts build --entry "[foo.ts," "bar.ts]"
1188
+ ✖ Don't use quotes around array elements.
1189
+ ✖ Also don't use spaces — unless you wrap the whole array in quotes.
1190
+ ⚠ Array argument --entry: Detected possible shell splitting of bracketed value ('[foo.ts,').
1191
+ ⚠ If you intended to pass a bracketed list, quote the whole value like: --entry "[a, b, c]"
1192
+ ```
1193
+
1194
+ #### 7. All together
1195
+
1196
+ ```bash
1197
+ mycli input.txt --verbose --name Alice --count 3 --tags foo --tags bar
1198
+ # → args = {
1199
+ # input: "input.txt",
1200
+ # verbose: true,
1201
+ # name: "Alice",
1202
+ # count: 3,
1203
+ # tags: ["foo", "bar"]
1204
+ # }
1205
+ ```
1206
+
1207
+ #### 8. Value Validation with `allowed`
1208
+
1209
+ All argument types support an optional `allowed` property that restricts which values can be passed:
1210
+
1211
+ ```ts
1212
+ const main = defineCommand({
1213
+ args: {
1214
+ // Only allow specific string values
1215
+ mode: {
1216
+ type: "string",
1217
+ allowed: ["development", "production", "test"],
1218
+ description: "The mode to run in"
1219
+ },
1220
+
1221
+ // Only allow specific boolean values (e.g. if you only want true)
1222
+ force: {
1223
+ type: "boolean",
1224
+ allowed: [true],
1225
+ description: "Force the operation"
1226
+ },
1227
+
1228
+ // Only allow specific numbers
1229
+ level: {
1230
+ type: "number",
1231
+ allowed: [1, 2, 3],
1232
+ description: "The level to use"
1233
+ },
1234
+
1235
+ // Only allow specific values in an array
1236
+ tags: {
1237
+ type: "array",
1238
+ allowed: ["web", "api", "mobile"],
1239
+ description: "Tags to apply"
1240
+ },
1241
+
1242
+ // Only allow specific positional values
1243
+ action: {
1244
+ type: "positional",
1245
+ allowed: ["build", "serve", "test"],
1246
+ description: "The action to perform"
1247
+ }
1248
+ }
1249
+ });
1250
+ ```
1251
+
1252
+ If someone tries to pass a value that's not in the `allowed` list, they'll get a helpful error message:
1253
+
1254
+ ```bash
1255
+ mycli --mode staging
1256
+ # Error: Invalid value for --mode: staging. Allowed values are: development, production, test
1257
+
1258
+ mycli --level 4
1259
+ # Error: Invalid value for --level: 4. Allowed values are: 1, 2, 3
1260
+
1261
+ mycli --tags desktop
1262
+ # Error: Invalid value in array --tags: desktop. Allowed values are: web, api, mobile
1263
+ ```
1264
+
1265
+ The validation happens after type casting, so for example with numbers, the input will first be converted to a number and then checked against the allowed list.
1266
+
1267
+ ## Typed Commands System
1268
+
1269
+ The typed commands system provides TypeScript intellisense and type safety for rempts launcher usage while maintaining dynamic code execution.
1270
+
1271
+ - 🎯 **TypeScript Intellisense**: Full autocomplete for command names and arguments
1272
+ - 🔒 **Type Safety**: Compile-time checking for argument types and required fields
1273
+ - ⚡ **Dynamic Execution**: Commands are still loaded and executed dynamically
1274
+ - 📝 **Automatic Sync**: Utility script to keep types in sync with actual command definitions
1275
+
1276
+ ### Usage
1277
+
1278
+ #### Basic Usage
1279
+
1280
+ ```typescript
1281
+ import { callCmd } from "~/app/cmds";
1282
+
1283
+ // Simple command with typed arguments
1284
+ await callCmd("pub", { dev: true });
1285
+
1286
+ // Command with multiple arguments
1287
+ await callCmd("check", {
1288
+ directory: "src",
1289
+ checks: "missing-deps,file-extensions",
1290
+ strict: true,
1291
+ json: false
1292
+ });
1293
+
1294
+ // Command with no arguments
1295
+ await callCmd("update");
1296
+
1297
+ // Generators with typed arguments
1298
+ await callCmd("rempts", {
1299
+ init: "new-cmd another-cmd",
1300
+ overwrite: true,
1301
+ outFile: "src/app/cmds.ts"
1302
+ });
1303
+ ```
1304
+
1305
+ #### Advanced Usage
1306
+
1307
+ ```typescript
1308
+ import { getTypedCmd } from "~/app/cmds";
1309
+
1310
+ // Get command instance for more control
1311
+ const { command, run } = await getTypedCmd("magic");
1312
+
1313
+ console.log(`Running: ${command.meta.name}`);
1314
+ console.log(`Description: ${command.meta.description}`);
1315
+
1316
+ await run({
1317
+ targets: ["dist-npm", "dist-jsr"],
1318
+ concurrency: 4,
1319
+ stopOnError: true
1320
+ });
1321
+ ```
1322
+
1323
+ #### TypeScript Benefits
1324
+
1325
+ ##### 1. Command Name Autocomplete
1326
+
1327
+ When you type `callCmd("`, TypeScript will show all available commands.
1328
+
1329
+ ##### 2. Argument Intellisense
1330
+
1331
+ When you type the arguments object, you get full autocomplete for:
1332
+
1333
+ - Argument names
1334
+ - Argument types
1335
+ - Required vs optional fields
1336
+
1337
+ ##### 3. Type Validation
1338
+
1339
+ ```typescript
1340
+ // ✅ Correct usage
1341
+ await callCmd("create", {
1342
+ mode: "files", // Only "template" | "files" allowed
1343
+ multiple: true // boolean
1344
+ });
1345
+
1346
+ // TypeScript errors
1347
+ await callCmd("create", {
1348
+ mode: "invalid", // Error: not assignable to type
1349
+ multiple: "yes" // Error: string not assignable to boolean
1350
+ });
1351
+ ```
1352
+
1353
+ ##### 4. Required Field Checking
1354
+
1355
+ ```typescript
1356
+ // ✅ Required field provided
1357
+ await callCmd("magic", {
1358
+ targets: ["dist-npm"] // Required field
1359
+ });
1360
+
1361
+ // ❌ TypeScript error: missing required field 'targets'
1362
+ await callCmd("magic", {
1363
+ concurrency: 4
1364
+ });
1365
+ ```
1366
+
1367
+ ### Maintaining the System
1368
+
1369
+ #### Adding New Commands
1370
+
1371
+ 1. Create your command in `src/app/<command-name>/cmd.ts` using `defineCommand` and `defineArgs`
1372
+ 2. Run the generator: `dler rempts --overwrite`
1373
+ 3. The `CommandArgsMap` interface in `src/app/cmds.ts` will be automatically updated
1374
+
1375
+ #### Manual Updates
1376
+
1377
+ The `CommandArgsMap` interface is auto-generated. If you need custom types, you can add manual type assertions (it is more recommended to edit your command file instead and regenerate the types):
1378
+
1379
+ ```typescript
1380
+ interface CommandArgsMap {
1381
+ myCommand: {
1382
+ // Use union types for specific values
1383
+ mode: "development" | "production";
1384
+
1385
+ // Use template literal types for patterns
1386
+ version: `${number}.${number}.${number}`;
1387
+
1388
+ // Use branded types for validation
1389
+ port: number & { __brand: "Port" };
1390
+ };
1391
+ }
1392
+ ```
1393
+
1394
+ ### Migration from Old System
1395
+
1396
+ #### Before (Old System, still supported)
1397
+
1398
+ ```typescript
1399
+ import { runCmd } from "@reliverse/rempts";
1400
+ import { getPubCmd } from "./app/cmds";
1401
+
1402
+ // No type safety, string-based arguments
1403
+ await runCmd(await getPubCmd(), [`--dev=${isDev}`]);
1404
+ ```
1405
+
1406
+ ### After (New System)
1407
+
1408
+ ```typescript
1409
+ import { callCmd } from "./app/cmds";
1410
+
1411
+ // Full type safety and intellisense
1412
+ await callCmd("pub", { dev: isDev });
1413
+ ```
1414
+
1415
+ ### Implementation Details
1416
+
1417
+ The system works by:
1418
+
1419
+ 1. **Command Loading**: Commands are still loaded dynamically using `loadCommand()`
1420
+ 2. **Argument Conversion**: Typed arguments are converted to string array format that `runCmd` expects
1421
+ 3. **Type Mapping**: `CommandArgsMap` interface maps command names to their argument types
1422
+ 4. **Generic Types**: `callCmd<T extends keyof CommandArgsMap>` provides type safety
1423
+
1424
+ ### Generator Usage
1425
+
1426
+ The typed command system also supports calling generators with full intellisense:
1427
+
1428
+ #### Creating New Commands
1429
+
1430
+ ```typescript
1431
+ // Create new commands with typed arguments
1432
+ await callCmd("rempts", {
1433
+ init: "auth login logout", // Commands to create
1434
+ overwrite: true, // Overwrite existing
1435
+ outFile: "src/app/cmds.ts" // Export file path
1436
+ });
1437
+
1438
+ // Create commands in custom location
1439
+ await callCmd("rempts", {
1440
+ init: "api-handler",
1441
+ customCmdsRoot: "src/modules/api",
1442
+ outFile: "src/modules/api/exports.ts",
1443
+ overwrite: true
1444
+ });
1445
+ ```
1446
+
1447
+ #### Regenerating Exports
1448
+
1449
+ ```typescript
1450
+ // Regenerate exports file only
1451
+ await callCmd("rempts", {
1452
+ overwrite: true,
1453
+ outFile: "src/app/cmds.ts"
1454
+ });
1455
+
1456
+ // Generate exports for specific directories
1457
+ await callCmd("rempts", {
1458
+ cmdDirs: ["build", "pub", "magic"],
1459
+ outFile: "src/app/core-cmds.ts",
1460
+ overwrite: true
1461
+ });
1462
+ ```
1463
+
1464
+ #### Batch Operations
1465
+
1466
+ ```typescript
1467
+ // Create multiple commands programmatically
1468
+ const modules = ["auth", "db", "api", "deploy"];
1469
+
1470
+ for (const module of modules) {
1471
+ await callCmd("rempts", {
1472
+ init: `${module}-create ${module}-update ${module}-delete`,
1473
+ customCmdsRoot: `src/modules/${module}`,
1474
+ outFile: `src/modules/${module}/cmds.ts`,
1475
+ overwrite: true
1476
+ });
1477
+ }
1478
+ ```
1479
+
1480
+ ## Contributing
1481
+
1482
+ Bug report? Prompt idea? Want to build the best DX possible?
1483
+
1484
+ You're in the right place! Please help us make the best CLI toolkit possible.
1485
+
1486
+ ### Notices For Contributors
1487
+
1488
+ **TypeScript Support**:
1489
+
1490
+ All APIs are fully typed. See [`src/types.ts`](./src/types.ts) for advanced customization and type inference.
1491
+
1492
+ **Examples**:
1493
+
1494
+ - **Classic CLI:** [`example/launcher/classic.ts`](./example/launcher/classic.ts)
1495
+ - **Modern Minimal CLI:** [`example/launcher/modern.ts`](./example/launcher/modern.ts)
1496
+ - **Full Prompt Demo:** [`example/prompts/mod.ts`](./example/prompts/mod.ts)
1497
+
1498
+ **Components and Utilities**:
1499
+
1500
+ - **components/**: All prompt UIs, CLI output, launcher logic, etc.
1501
+ - **utils/**: Color, error, validation, streaming, and system helpers.
1502
+ - **hooks/**: Useful hooks for prompt state and effects.
1503
+
1504
+ ### Helpful Links
1505
+
1506
+ - [CLI application with the Node.js Readline module](https://dev.to/camptocamp-geo/cli-application-with-the-nodejs-readline-module-48ic)
1507
+
1508
+ ## TODO
1509
+
1510
+ - [ ] migrate to `dler libs` in the future (all main components will be published as separate packages; `@reliverse/rempts` will be a wrapper for all of them)
1511
+ - [ ] migrate all tests to `bun:test`
1512
+
1513
+ ## Related
1514
+
1515
+ - [`@reliverse/rse`](https://npmjs.com/package/@reliverse/rse) – CLI-first toolkit for fullstack workflows
1516
+ - [`@reliverse/relinka`](https://npmjs.com/package/@reliverse/relinka) – Styled CLI logs, steps, and symbols
1517
+
1518
+ ## Shoutouts
1519
+
1520
+ - [citty](https://github.com/unjs/citty#readme) - launcher design inspiration
1521
+
1522
+ ## Support
1523
+
1524
+ Bug report? Prompt idea? Want to build the best DX possible?
1525
+
1526
+ You're in the right place:
1527
+
1528
+ - ✨ [Star the repo](https://github.com/reliverse/dler)
1529
+ - 💬 [Join the Discord](https://discord.gg/3GawfWfAPe)
1530
+ - ❤️ [Sponsor @blefnk](https://github.com/sponsors/blefnk)
1531
+
1532
+ ## License
1533
+
1534
+ 💖 MIT (see [LICENSE](./LICENSE) and [LICENCES](./LICENSES)) © [blefnk (Nazar Kornienko)](https://github.com/blefnk)