@reliverse/rempts 1.7.0 β 1.7.1
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.
- package/README.md +235 -88
- package/bin/components/launcher/launcher-mod.d.ts +44 -0
- package/bin/components/launcher/launcher-mod.js +131 -115
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,94 +4,138 @@
|
|
|
4
4
|
|
|
5
5
|
[π¬ Discord](https://discord.gg/3GawfWfAPe) β [π¦ NPM](https://npmjs.com/package/@reliverse/rempts) β [π§ Docs](https://docs.reliverse.org/reliverse/rempts) β [π JSR](https://jsr.io/@reliverse/rempts) β [β¨ GitHub](https://github.com/reliverse/rempts)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- π File-based commands (optional)
|
|
9
|
+
- π« Rempts prevents you from fighting with your CLI tool
|
|
10
|
+
- β¨ Rempts is your end-to-end CLI UI + command framework
|
|
11
|
+
- πͺ Made for DX precision and high-context terminal UX
|
|
12
|
+
- π File-based commands (app router style by default)
|
|
13
|
+
- ποΈ Prompt engine that *feels* modern, actually is
|
|
16
14
|
- π§ Type-safe from args to prompts
|
|
17
|
-
- π¨ Customizable themes, styled output
|
|
18
|
-
- π§© Router + argument parser built-in
|
|
19
15
|
- β‘ Blazing-fast, no runtime baggage
|
|
16
|
+
- π§© Router + argument parser built-in
|
|
17
|
+
- π¨ Customizable themes, styled output
|
|
18
|
+
- π¨ Crash-safe (Ctrl+C, SIGINT, errors)
|
|
20
19
|
- πͺ Minimal API surface, max expressiveness
|
|
21
|
-
- ποΈ Prompt engine that *feels* modern, actually is
|
|
22
20
|
- π§ͺ Scriptable for testing, stable for production
|
|
23
|
-
-
|
|
24
|
-
-
|
|
21
|
+
- π Automatic commands creation (via `dler init --cmd my-cool-cmd`)
|
|
22
|
+
- ποΈ No more hacking together `inquirer`, `citty`, `commander`, `chalk`
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun add @reliverse/rempts
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage Examples
|
|
31
|
+
|
|
32
|
+
- [Prompts](#prompts)
|
|
33
|
+
- [Launcher](#launcher)
|
|
25
34
|
|
|
26
35
|
## Screenshot
|
|
27
36
|
|
|
28
|
-

|
|
37
|
+

|
|
29
38
|
|
|
30
|
-
##
|
|
39
|
+
## API Overview
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
- **Argument**: An argument is a value that is passed to a command.
|
|
34
|
-
- **Flag**: A flag is a boolean argument that is used to enable or disable a feature.
|
|
35
|
-
- **Option**: An option is a named argument that is used to configure a command.
|
|
41
|
+
All main prompts APIs are available from the package root:
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
```ts
|
|
44
|
+
import {
|
|
45
|
+
// ...prompts
|
|
46
|
+
defineCommand, runMain, defineArgs,
|
|
47
|
+
inputPrompt, selectPrompt, multiselectPrompt, numberPrompt,
|
|
48
|
+
confirmPrompt, togglePrompt, spinnerTaskPrompt, progressTaskPrompt,
|
|
49
|
+
startPrompt, endPrompt, resultPrompt, nextStepsPrompt,
|
|
50
|
+
// ...hooks
|
|
51
|
+
useSpinner,
|
|
52
|
+
// ...launcher
|
|
53
|
+
runMain, defineCommand, defineArgs,
|
|
54
|
+
// ...types
|
|
55
|
+
// ...more
|
|
56
|
+
} from "@reliverse/rempts";
|
|
57
|
+
```
|
|
38
58
|
|
|
39
|
-
|
|
59
|
+
> See [`src/mod.ts`](./src/mod.ts) for the full list of exports.
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
Use `subCommands` in your command definition or let the launcher automatically load commands from a specified directory.
|
|
61
|
+
## Prompts
|
|
43
62
|
|
|
44
|
-
-
|
|
45
|
-
The launcher scans your specified `cmdsRootPath` for command files matching common patterns such as:
|
|
46
|
-
- `arg-cmdName.{ts,js}`
|
|
47
|
-
- `cmdName/index.{ts,js}`
|
|
48
|
-
- `cmdName/cmdName-mod.{ts,js}`
|
|
49
|
-
- And more β with automatic usage output if a command file is not found.
|
|
63
|
+
### Built-in Prompts
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
| Prompt | Description |
|
|
66
|
+
|---------------------------|-----------------------------------------------------------|
|
|
67
|
+
| `inputPrompt` | Single-line input (with mask support, e.g. for passwords) |
|
|
68
|
+
| `selectPrompt` | Single-choice radio menu |
|
|
69
|
+
| `multiselectPrompt` | Multi-choice checkbox menu |
|
|
70
|
+
| `numberPrompt` | Type-safe number input |
|
|
71
|
+
| `confirmPrompt` | Yes/No toggle |
|
|
72
|
+
| `togglePrompt` | Custom on/off toggles |
|
|
73
|
+
| `progressTaskPrompt` | Progress bar for async tasks |
|
|
74
|
+
| `resultPrompt` | Show results in a styled box |
|
|
75
|
+
| `nextStepsPrompt` | Show next steps in a styled list |
|
|
76
|
+
| `startPrompt`/`endPrompt` | Makes CLI start/end flows look nice |
|
|
77
|
+
| `spinnerTaskPrompt` | Async loader with spinner (possibly will be deprecated) |
|
|
78
|
+
| `datePrompt` | Date input with format validation |
|
|
79
|
+
| `anykeyPrompt` | Wait for any keypress |
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
Seamlessly combines positional and named arguments with zero configuration, auto-parsing booleans, strings, numbers, arrays, and even supporting negated flags like `--no-flag`.
|
|
81
|
+
### Hooks
|
|
59
82
|
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
| Hook | Description |
|
|
84
|
+
|--------------|--------------------|
|
|
85
|
+
| `useSpinner` | Start/stop spinner |
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
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.
|
|
87
|
+
### Notices
|
|
65
88
|
|
|
66
|
-
-
|
|
67
|
-
- You can define optional `setup` and `cleanup` functions in your main command. These hooks are called automatically before and after any command or subcommand runs (including file-based and programmatic subcommands). This is perfect for initializing resources or cleaning up after execution.
|
|
89
|
+
- `setup`/`cleanup` are now `onCmdStart`/`onCmdEnd` (old names still work for now).
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
- The launcher inspects your available subcommands and their argument definitions, then prints a plausible example CLI invocation for a random subcommand directly in the help output. This helps users understand real-world usage at a glance.
|
|
91
|
+
### Prompts Usage Example
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- File-based subcommands are auto-discovered from your filesystem, while programmatic subcommands can be defined inline in your main command.
|
|
93
|
+
```ts
|
|
94
|
+
import { relinka } from "@reliverse/relinka";
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
import {
|
|
97
|
+
startPrompt,
|
|
98
|
+
inputPrompt,
|
|
99
|
+
selectPrompt,
|
|
100
|
+
defineCommand,
|
|
101
|
+
runMain
|
|
102
|
+
} from "@reliverse/rempts";
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
async function main() {
|
|
105
|
+
await startPrompt({ title: "Project Setup" });
|
|
81
106
|
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
const name = await inputPrompt({
|
|
108
|
+
title: "What's your project name?",
|
|
109
|
+
defaultValue: "my-cool-project",
|
|
110
|
+
});
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
const framework = await selectPrompt({
|
|
113
|
+
title: "Pick your framework",
|
|
114
|
+
options: [
|
|
115
|
+
{ value: "next", label: "Next.js" },
|
|
116
|
+
{ value: "svelte", label: "SvelteKit" },
|
|
117
|
+
{ value: "start", label: "TanStack Start" },
|
|
118
|
+
],
|
|
119
|
+
defaultValue: "next",
|
|
120
|
+
});
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
console.log("Your result:", { name, framework });
|
|
123
|
+
};
|
|
90
124
|
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
await main();
|
|
126
|
+
```
|
|
93
127
|
|
|
94
|
-
|
|
128
|
+
## Launcher
|
|
129
|
+
|
|
130
|
+
### Terminology
|
|
131
|
+
|
|
132
|
+
- **Launcher/Router**: The main entry point for your CLI. Visit [CLI Launcher (Router)](#cli-launcher-router) section to learn more.
|
|
133
|
+
- **Command/Subcommand**: A command is a function that defines the behavior of a CLI.
|
|
134
|
+
- **Argument**: An argument is a value that is passed to a command.
|
|
135
|
+
- **Flag**: A flag is a boolean argument that is used to enable or disable a feature.
|
|
136
|
+
- **Option**: An option is a named argument that is used to configure a command.
|
|
137
|
+
|
|
138
|
+
#### Launcher Usage Example
|
|
95
139
|
|
|
96
140
|
βΌοΈ Go to [Usage Examples](#usage-examples) section for a more detailed example.
|
|
97
141
|
|
|
@@ -106,10 +150,10 @@ const main = defineCommand({
|
|
|
106
150
|
version: "1.0.0",
|
|
107
151
|
description: "Rempts Launcher Playground CLI",
|
|
108
152
|
},
|
|
109
|
-
|
|
153
|
+
onCmdStart() {
|
|
110
154
|
relinka("success", "Setup");
|
|
111
155
|
},
|
|
112
|
-
|
|
156
|
+
onCmdEnd() {
|
|
113
157
|
relinka("success", "Cleanup");
|
|
114
158
|
},
|
|
115
159
|
subCommands: {
|
|
@@ -126,7 +170,7 @@ await runMain(main);
|
|
|
126
170
|
await runMain(myCommand, {
|
|
127
171
|
fileBasedCmds: {
|
|
128
172
|
enable: true,
|
|
129
|
-
cmdsRootPath: "
|
|
173
|
+
cmdsRootPath: "my-cmds", // default is `./app`
|
|
130
174
|
},
|
|
131
175
|
// Optionally disable auto-exit to handle errors manually:
|
|
132
176
|
autoExit: false,
|
|
@@ -135,7 +179,7 @@ await runMain(myCommand, {
|
|
|
135
179
|
|
|
136
180
|
This flexibility allows you to easily build a rich, multi-command CLI with minimal boilerplate. The launcher even supports nested subcommands, making it simple to construct complex CLI applications.
|
|
137
181
|
|
|
138
|
-
|
|
182
|
+
#### File-Based Subcommands
|
|
139
183
|
|
|
140
184
|
Drop a `./src/cli/app/add/index.ts` and it's live.
|
|
141
185
|
|
|
@@ -169,22 +213,10 @@ export default defineCommand({
|
|
|
169
213
|
|
|
170
214
|
**Hint**:
|
|
171
215
|
|
|
172
|
-
- Install `bun
|
|
173
|
-
- Use `
|
|
216
|
+
- Install `bun add -D @reliverse/dler`
|
|
217
|
+
- Use `dler init --cmd cmd1 cmd2` to init commands for rempts launcher's automatically
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
- π§ `inputPrompt` β Single-line, password, masked
|
|
178
|
-
- β
`selectPrompt` β Radio menu
|
|
179
|
-
- π§° `multiselectPrompt` β Checkbox menu
|
|
180
|
-
- π’ `numberPrompt` β Type-safe number input
|
|
181
|
-
- π `confirmPrompt` β Yes/No toggle
|
|
182
|
-
- π₯ `togglePrompt` β Custom on/off toggles
|
|
183
|
-
- β³ `spinnerPrompt` β Async loaders with status
|
|
184
|
-
- π `logPrompt` β Styled logs / steps
|
|
185
|
-
- π§Ό `clearPrompt` β Clears console with style
|
|
186
|
-
|
|
187
|
-
## π§± Minimal, Functional API
|
|
219
|
+
### Advanced Minimal API
|
|
188
220
|
|
|
189
221
|
```ts
|
|
190
222
|
defineCommand({
|
|
@@ -207,14 +239,14 @@ defineCommand({
|
|
|
207
239
|
- Default values, validations, descriptions
|
|
208
240
|
- Full help rendering from metadata
|
|
209
241
|
|
|
210
|
-
|
|
242
|
+
### Theming + Customization
|
|
211
243
|
|
|
212
244
|
- Built-in output formatter and logger
|
|
213
245
|
- Override styles via prompt options
|
|
214
246
|
- Smart layout for small terminals
|
|
215
247
|
- Looks great in plain scripts or full CLI apps
|
|
216
248
|
|
|
217
|
-
|
|
249
|
+
### Playground
|
|
218
250
|
|
|
219
251
|
```bash
|
|
220
252
|
bun i -g @reliverse/rempts-cli
|
|
@@ -233,9 +265,9 @@ bun dev # supported options: name
|
|
|
233
265
|
- Both `rempts examples` from @reliverse/rempts and `bun dev` (which is the same thing) are themselves examples of `launcher` functionality.
|
|
234
266
|
- This launcher will show you a `multiselectPrompt()` where you can choose which CLI prompts you want to play with.
|
|
235
267
|
|
|
236
|
-
|
|
268
|
+
### Launcher Usage Examples
|
|
237
269
|
|
|
238
|
-
|
|
270
|
+
#### Minimal Usage Example
|
|
239
271
|
|
|
240
272
|
**1 Create a `src/mod.ts` file:**
|
|
241
273
|
|
|
@@ -262,7 +294,13 @@ export default defineCommand({
|
|
|
262
294
|
});
|
|
263
295
|
```
|
|
264
296
|
|
|
265
|
-
|
|
297
|
+
**4. Test it:**
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
bun src/mod.ts
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Medium Usage Example
|
|
266
304
|
|
|
267
305
|
```ts
|
|
268
306
|
import { defineCommand, runMain } from "@reliverse/rempts";
|
|
@@ -279,7 +317,7 @@ const main = defineCommand({
|
|
|
279
317
|
await runMain(main);
|
|
280
318
|
```
|
|
281
319
|
|
|
282
|
-
|
|
320
|
+
#### Classic Usage Example
|
|
283
321
|
|
|
284
322
|
```ts
|
|
285
323
|
import { relinka } from "@reliverse/relinka";
|
|
@@ -307,7 +345,7 @@ const main = defineCommand({
|
|
|
307
345
|
},
|
|
308
346
|
async run({ args }) {
|
|
309
347
|
await startPrompt({
|
|
310
|
-
title: "
|
|
348
|
+
title: "Project Setup",
|
|
311
349
|
});
|
|
312
350
|
|
|
313
351
|
const name = await inputPrompt({
|
|
@@ -331,7 +369,7 @@ const main = defineCommand({
|
|
|
331
369
|
await runMain(main);
|
|
332
370
|
```
|
|
333
371
|
|
|
334
|
-
|
|
372
|
+
#### Advanced Usage Example
|
|
335
373
|
|
|
336
374
|
```ts
|
|
337
375
|
import { relinka } from "@reliverse/relinka";
|
|
@@ -409,7 +447,7 @@ const mainCommand = defineCommand({
|
|
|
409
447
|
|
|
410
448
|
// Begin interactive session with a prompt.
|
|
411
449
|
await startPrompt({
|
|
412
|
-
title: "
|
|
450
|
+
title: "Project Setup",
|
|
413
451
|
});
|
|
414
452
|
|
|
415
453
|
// Ask for the project name, falling back to provided argument or a default.
|
|
@@ -469,6 +507,97 @@ await runMain(mainCommand, {
|
|
|
469
507
|
});
|
|
470
508
|
```
|
|
471
509
|
|
|
510
|
+
### CLI Launcher (Router)
|
|
511
|
+
|
|
512
|
+
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 subcommands and file-based routing, so you can structure your CLI however you like. It automatically detects and loads subcommands 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:
|
|
513
|
+
|
|
514
|
+
- **File-Based & Defined Subcommands:**
|
|
515
|
+
Use `subCommands` in your command definition or let the launcher automatically load commands from a specified directory.
|
|
516
|
+
|
|
517
|
+
- **Automatic Command Detection:**
|
|
518
|
+
The launcher scans your specified `cmdsRootPath` for command files matching common patterns such as:
|
|
519
|
+
- `arg-cmdName.{ts,js}`
|
|
520
|
+
- `cmdName/index.{ts,js}`
|
|
521
|
+
- `cmdName/cmdName-mod.{ts,js}`
|
|
522
|
+
- And more β with automatic usage output if a command file is not found.
|
|
523
|
+
|
|
524
|
+
- **Built-In Flag Handling:**
|
|
525
|
+
Automatically processes global flags such as:
|
|
526
|
+
- `--help` and `-h` to show usage details.
|
|
527
|
+
- `--version` and `-v` to display version information.
|
|
528
|
+
- `--debug` for verbose logging during development.
|
|
529
|
+
|
|
530
|
+
- **Unified Argument Parsing:**
|
|
531
|
+
Seamlessly combines positional and named arguments with zero configuration, auto-parsing booleans, strings, numbers, arrays, and even supporting negated flags like `--no-flag`.
|
|
532
|
+
|
|
533
|
+
- **Customizable Behavior:**
|
|
534
|
+
Options such as `fileBasedCmds.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.
|
|
535
|
+
|
|
536
|
+
- **Error Management & Usage Output:**
|
|
537
|
+
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.
|
|
538
|
+
|
|
539
|
+
- **Lifecycle Hooks:**
|
|
540
|
+
You can define optional lifecycle hooks in your main command:
|
|
541
|
+
- `onLauncherStart` and `onLauncherEnd` (global, called once per CLI process)
|
|
542
|
+
- `onCmdStart` and `onCmdEnd` (per-subcommand, called before/after each subcommand, but NOT for the main `run()` handler)
|
|
543
|
+
|
|
544
|
+
**Global Hooks:**
|
|
545
|
+
- `onLauncherStart`: Called once, before any command/subcommand/run() is executed.
|
|
546
|
+
- `onLauncherEnd`: Called once, after all command/subcommand/run() logic is finished (even if an error occurs).
|
|
547
|
+
|
|
548
|
+
**Per-Subcommand Hooks:**
|
|
549
|
+
- `onCmdStart`: Called before each subcommand (not for main `run()`).
|
|
550
|
+
- `onCmdEnd`: Called after each subcommand (not for main `run()`).
|
|
551
|
+
|
|
552
|
+
This means:
|
|
553
|
+
- If your CLI has multiple subcommands, `onCmdStart` and `onCmdEnd` will be called for each subcommand invocation, not just once for the whole CLI process.
|
|
554
|
+
- If your main command has a `run()` handler (and no subcommand is invoked), these hooks are **not** called; use the `run()` handler itself or the global hooks for such logic.
|
|
555
|
+
- This allows you to perform setup/teardown logic specific to each subcommand execution.
|
|
556
|
+
- If you want logic to run only once for the entire CLI process, use `onLauncherStart` and `onLauncherEnd`.
|
|
557
|
+
|
|
558
|
+
**Example:**
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
const main = defineCommand({
|
|
562
|
+
onLauncherStart() { relinka('info', 'Global setup (once per process)'); },
|
|
563
|
+
onLauncherEnd() { relinka('info', 'Global cleanup (once per process)'); },
|
|
564
|
+
onCmdStart() { relinka('info', 'Setup for each subcommand'); },
|
|
565
|
+
onCmdEnd() { relinka('info', 'Cleanup for each subcommand'); },
|
|
566
|
+
subCommands: { ... },
|
|
567
|
+
run() { relinka('info', 'Main run handler (no subcommand)'); },
|
|
568
|
+
});
|
|
569
|
+
// onLauncherStart/onLauncherEnd are called once per process
|
|
570
|
+
// onCmdStart/onCmdEnd are called for every subcommand (not for main run())
|
|
571
|
+
// If you want per-run() logic, use the run() handler or global hooks
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
> **Note:** 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 `onCmdStart` and `onCmdEnd` going forward.
|
|
575
|
+
|
|
576
|
+
- **Dynamic Usage Examples:**
|
|
577
|
+
- The launcher inspects your available subcommands and their argument definitions, then prints a plausible example CLI invocation for a random subcommand directly in the help output. This helps users understand real-world usage at a glance.
|
|
578
|
+
|
|
579
|
+
- **File-Based & Programmatic Subcommands:**
|
|
580
|
+
- Both file-based and object subcommands are fully supported. The launcher can introspect their argument definitions and metadata for help, usage, and validation.
|
|
581
|
+
- File-based subcommands are auto-discovered from your filesystem, while programmatic subcommands can be defined inline in your main command.
|
|
582
|
+
|
|
583
|
+
- **Context-Aware Help Output:**
|
|
584
|
+
- The help/usage output adapts to your CLI's structure, showing available subcommands, their aliases, argument details, and even dynamic usage examples. It also displays global options and context-specific error messages.
|
|
585
|
+
|
|
586
|
+
- **Error Handling:**
|
|
587
|
+
- 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.
|
|
588
|
+
|
|
589
|
+
- **Unified Argument Parsing:**
|
|
590
|
+
- All arguments (positional, named, boolean, string, number, array) are parsed and validated automatically. Negated flags (like `--no-flag`) are supported out of the box.
|
|
591
|
+
|
|
592
|
+
- **Extensible & Flexible:**
|
|
593
|
+
- 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.
|
|
594
|
+
|
|
595
|
+
- **Bun & Node.js Support:**
|
|
596
|
+
- The launcher is designed to work in both Bun and Node.js environments, so you can use it in any modern JavaScript/TypeScript project.
|
|
597
|
+
|
|
598
|
+
- **Prompt-First, Modern UX:**
|
|
599
|
+
- The launcher integrates tightly with the prompt engine, so you can build interactive, delightful CLIs with minimal effort.
|
|
600
|
+
|
|
472
601
|
## Contributing
|
|
473
602
|
|
|
474
603
|
Bug report? Prompt idea? Want to build the best DX possible?
|
|
@@ -481,6 +610,24 @@ You're in the right place:
|
|
|
481
610
|
|
|
482
611
|
> *No classes. No magic. Just clean, composable tools for CLI devs.*
|
|
483
612
|
|
|
613
|
+
### Notices For Contributors
|
|
614
|
+
|
|
615
|
+
**TypeScript Support**:
|
|
616
|
+
|
|
617
|
+
All APIs are fully typed. See [`src/types.ts`](./src/types.ts) for advanced customization and type inference.
|
|
618
|
+
|
|
619
|
+
**Examples**:
|
|
620
|
+
|
|
621
|
+
- **Classic CLI:** [`example/launcher/classic.ts`](./example/launcher/classic.ts)
|
|
622
|
+
- **Modern Minimal CLI:** [`example/launcher/modern.ts`](./example/launcher/modern.ts)
|
|
623
|
+
- **Full Prompt Demo:** [`example/prompts/mod.ts`](./example/prompts/mod.ts)
|
|
624
|
+
|
|
625
|
+
**Components and Utilities**:
|
|
626
|
+
|
|
627
|
+
- **components/**: All prompt UIs, CLI output, launcher logic, etc.
|
|
628
|
+
- **utils/**: Color, error, validation, streaming, and system helpers.
|
|
629
|
+
- **hooks/**: Useful hooks for prompt state and effects.
|
|
630
|
+
|
|
484
631
|
### Helpful Links
|
|
485
632
|
|
|
486
633
|
- [CLI application with the Node.js Readline module](https://dev.to/camptocamp-geo/cli-application-with-the-nodejs-readline-module-48ic)
|
|
@@ -58,16 +58,60 @@ type DefineCommandOptions<A extends ArgDefinitions = EmptyArgs> = {
|
|
|
58
58
|
args?: A;
|
|
59
59
|
run?: CommandRun<InferArgTypes<A>>;
|
|
60
60
|
subCommands?: SubCommandsMap;
|
|
61
|
+
/**
|
|
62
|
+
* Called before the command or subcommand runs
|
|
63
|
+
*/
|
|
64
|
+
onCmdStart?: () => void | Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Called after the command or subcommand finishes
|
|
67
|
+
*/
|
|
68
|
+
onCmdEnd?: () => void | Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Use onCmdStart instead
|
|
71
|
+
*/
|
|
61
72
|
setup?: () => void | Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* @deprecated Use onCmdEnd instead
|
|
75
|
+
*/
|
|
62
76
|
cleanup?: () => void | Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Called once per CLI process, before any command/subcommand/run() is executed
|
|
79
|
+
*/
|
|
80
|
+
onLauncherStart?: () => void | Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Called once per CLI process, after all command/subcommand/run() logic is finished
|
|
83
|
+
*/
|
|
84
|
+
onLauncherEnd?: () => void | Promise<void>;
|
|
63
85
|
};
|
|
64
86
|
export type Command<A extends ArgDefinitions = EmptyArgs> = {
|
|
65
87
|
meta?: CommandMeta;
|
|
66
88
|
args: A;
|
|
67
89
|
run?: CommandRun<InferArgTypes<A>>;
|
|
68
90
|
subCommands?: SubCommandsMap;
|
|
91
|
+
/**
|
|
92
|
+
* Called before the command or subcommand runs
|
|
93
|
+
*/
|
|
94
|
+
onCmdStart?: () => void | Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Called after the command or subcommand finishes
|
|
97
|
+
*/
|
|
98
|
+
onCmdEnd?: () => void | Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* @deprecated Use onCmdStart instead
|
|
101
|
+
*/
|
|
69
102
|
setup?: () => void | Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Use onCmdEnd instead
|
|
105
|
+
*/
|
|
70
106
|
cleanup?: () => void | Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Called once per CLI process, before any command/subcommand/run() is executed
|
|
109
|
+
*/
|
|
110
|
+
onLauncherStart?: () => void | Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Called once per CLI process, after all command/subcommand/run() logic is finished
|
|
113
|
+
*/
|
|
114
|
+
onLauncherEnd?: () => void | Promise<void>;
|
|
71
115
|
};
|
|
72
116
|
export type InferArgTypes<A extends ArgDefinitions> = {
|
|
73
117
|
[K in keyof A]: A[K] extends PositionalArgDefinition ? string : A[K] extends BooleanArgDefinition ? boolean : A[K] extends StringArgDefinition ? string : A[K] extends NumberArgDefinition ? number : A[K] extends {
|
|
@@ -57,13 +57,22 @@ function isFlag(str) {
|
|
|
57
57
|
return str.startsWith("-");
|
|
58
58
|
}
|
|
59
59
|
export function defineCommand(options) {
|
|
60
|
+
const onCmdStart = options.onCmdStart || options.setup;
|
|
61
|
+
const onCmdEnd = options.onCmdEnd || options.cleanup;
|
|
62
|
+
const onLauncherStart = options.onLauncherStart;
|
|
63
|
+
const onLauncherEnd = options.onLauncherEnd;
|
|
60
64
|
return {
|
|
61
65
|
meta: options.meta,
|
|
62
66
|
args: options.args || {},
|
|
63
67
|
run: options.run,
|
|
64
68
|
subCommands: options.subCommands,
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
onCmdStart,
|
|
70
|
+
onCmdEnd,
|
|
71
|
+
onLauncherStart,
|
|
72
|
+
onLauncherEnd,
|
|
73
|
+
// Backward-compatible aliases
|
|
74
|
+
setup: onCmdStart,
|
|
75
|
+
cleanup: onCmdEnd
|
|
67
76
|
};
|
|
68
77
|
}
|
|
69
78
|
let _cachedDefaultCliName;
|
|
@@ -228,146 +237,153 @@ export async function showUsage(command, parserOptions = {}, displayNotFoundMess
|
|
|
228
237
|
}
|
|
229
238
|
}
|
|
230
239
|
export async function runMain(command, parserOptions = {}) {
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
if (typeof command.onLauncherStart === "function")
|
|
241
|
+
await command.onLauncherStart();
|
|
242
|
+
try {
|
|
243
|
+
if (!parserOptions.fileBasedCmds && !command.subCommands) {
|
|
244
|
+
let callerDir = process.cwd();
|
|
245
|
+
let callerFile;
|
|
246
|
+
try {
|
|
247
|
+
const err = new Error();
|
|
248
|
+
const stack = err.stack?.split("\n");
|
|
249
|
+
if (stack) {
|
|
250
|
+
for (const line of stack) {
|
|
251
|
+
const match = /\((.*):(\d+):(\d+)\)/.exec(line) || /at (.*):(\d+):(\d+)/.exec(line);
|
|
252
|
+
if (match?.[1] && !match[1].includes("launcher-mod")) {
|
|
253
|
+
callerFile = match[1];
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
243
256
|
}
|
|
244
257
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
`runMain() should not be called from a file-based subcommand: ${rel}
|
|
258
|
+
if (callerFile) {
|
|
259
|
+
callerDir = path.dirname(callerFile);
|
|
260
|
+
const rel = path.relative(process.cwd(), callerFile);
|
|
261
|
+
if (/app[/][^/]+[/]cmd\.(ts|js)$/.test(rel)) {
|
|
262
|
+
relinka(
|
|
263
|
+
"error",
|
|
264
|
+
`runMain() should not be called from a file-based subcommand: ${rel}
|
|
253
265
|
This can cause recursion or unexpected behavior.
|
|
254
266
|
Move your runMain() call to your main CLI entry file.`
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const mainEntry = process.argv[1] ? path.resolve(process.argv[1]) : void 0;
|
|
271
|
+
if (mainEntry && path.resolve(callerFile) !== mainEntry) {
|
|
272
|
+
relinka(
|
|
273
|
+
"error",
|
|
274
|
+
`runMain() should only be called from your main CLI entry file.
|
|
263
275
|
Detected: ${callerFile}
|
|
264
276
|
Main entry: ${mainEntry}
|
|
265
277
|
This can cause recursion or unexpected behavior.`
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
268
281
|
}
|
|
282
|
+
} catch (_e) {
|
|
269
283
|
}
|
|
270
|
-
|
|
284
|
+
const defaultCmdsRoot = path.resolve(callerDir, "app");
|
|
285
|
+
parserOptions.fileBasedCmds = {
|
|
286
|
+
enable: true,
|
|
287
|
+
cmdsRootPath: defaultCmdsRoot
|
|
288
|
+
};
|
|
271
289
|
}
|
|
272
|
-
const
|
|
273
|
-
parserOptions.
|
|
274
|
-
|
|
275
|
-
cmdsRootPath: defaultCmdsRoot
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
const rawArgv = process.argv.slice(2);
|
|
279
|
-
const autoExit = parserOptions.autoExit !== false;
|
|
280
|
-
if (!(parserOptions.fileBasedCmds?.enable || command.subCommands && Object.keys(command.subCommands).length > 0 || command.run)) {
|
|
281
|
-
relinka(
|
|
282
|
-
"error",
|
|
283
|
-
"Invalid CLI configuration: No file-based commands, subCommands, or run() handler are defined. This CLI will not do anything.\n\u2502 To fix: add file-based commands (./app), or provide at least one subCommand or a run() handler."
|
|
284
|
-
);
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
if (rawArgv[0] === "help") {
|
|
288
|
-
await showUsage(command, parserOptions);
|
|
289
|
-
if (autoExit) process.exit(0);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
await relinkaConfig;
|
|
293
|
-
if (checkHelp(rawArgv)) {
|
|
294
|
-
await showUsage(command, parserOptions);
|
|
295
|
-
if (autoExit) process.exit(0);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (checkVersion(rawArgv)) {
|
|
299
|
-
if (command.meta?.name) {
|
|
290
|
+
const rawArgv = process.argv.slice(2);
|
|
291
|
+
const autoExit = parserOptions.autoExit !== false;
|
|
292
|
+
if (!(parserOptions.fileBasedCmds?.enable || command.subCommands && Object.keys(command.subCommands).length > 0 || command.run)) {
|
|
300
293
|
relinka(
|
|
301
|
-
"
|
|
302
|
-
|
|
294
|
+
"error",
|
|
295
|
+
"Invalid CLI configuration: No file-based commands, subCommands, or run() handler are defined. This CLI will not do anything.\n\u2502 To fix: add file-based commands (./app), or provide at least one subCommand or a run() handler."
|
|
303
296
|
);
|
|
297
|
+
process.exit(1);
|
|
304
298
|
}
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
const fileBasedEnabled = parserOptions.fileBasedCmds?.enable;
|
|
309
|
-
if (fileBasedEnabled && rawArgv.length > 0 && !isFlag(rawArgv[0])) {
|
|
310
|
-
const [subName, ...subCmdArgv] = rawArgv;
|
|
311
|
-
try {
|
|
312
|
-
if (typeof command.setup === "function") await command.setup();
|
|
313
|
-
await runFileBasedSubCmd(
|
|
314
|
-
subName,
|
|
315
|
-
subCmdArgv,
|
|
316
|
-
parserOptions.fileBasedCmds,
|
|
317
|
-
parserOptions,
|
|
318
|
-
command.cleanup
|
|
319
|
-
);
|
|
299
|
+
if (rawArgv[0] === "help") {
|
|
300
|
+
await showUsage(command, parserOptions);
|
|
320
301
|
if (autoExit) process.exit(0);
|
|
321
302
|
return;
|
|
322
|
-
} catch (err) {
|
|
323
|
-
relinka("error", "Error loading file-based subcommand:", err.message);
|
|
324
|
-
if (autoExit) process.exit(1);
|
|
325
|
-
throw err;
|
|
326
303
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
subSpec = spec;
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
} catch (err) {
|
|
343
|
-
debugLog(`Error checking alias for subcommand ${key}:`, err);
|
|
304
|
+
await relinkaConfig;
|
|
305
|
+
if (checkHelp(rawArgv)) {
|
|
306
|
+
await showUsage(command, parserOptions);
|
|
307
|
+
if (autoExit) process.exit(0);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (checkVersion(rawArgv)) {
|
|
311
|
+
if (command.meta?.name) {
|
|
312
|
+
relinka(
|
|
313
|
+
"info",
|
|
314
|
+
`${command.meta?.name} ${command.meta?.version ? `v${command.meta?.version}` : ""}`
|
|
315
|
+
);
|
|
344
316
|
}
|
|
317
|
+
if (autoExit) process.exit(0);
|
|
318
|
+
return;
|
|
345
319
|
}
|
|
346
|
-
|
|
320
|
+
const fileBasedEnabled = parserOptions.fileBasedCmds?.enable;
|
|
321
|
+
if (fileBasedEnabled && rawArgv.length > 0 && !isFlag(rawArgv[0])) {
|
|
322
|
+
const [subName, ...subCmdArgv] = rawArgv;
|
|
347
323
|
try {
|
|
348
|
-
if (typeof command.
|
|
349
|
-
|
|
350
|
-
|
|
324
|
+
if (typeof command.onCmdStart === "function")
|
|
325
|
+
await command.onCmdStart();
|
|
326
|
+
await runFileBasedSubCmd(
|
|
327
|
+
subName,
|
|
351
328
|
subCmdArgv,
|
|
329
|
+
parserOptions.fileBasedCmds,
|
|
352
330
|
parserOptions,
|
|
353
|
-
command.
|
|
331
|
+
command.onCmdEnd
|
|
354
332
|
);
|
|
355
333
|
if (autoExit) process.exit(0);
|
|
356
334
|
return;
|
|
357
335
|
} catch (err) {
|
|
358
|
-
relinka("error", "Error
|
|
336
|
+
relinka("error", "Error loading file-based subcommand:", err.message);
|
|
359
337
|
if (autoExit) process.exit(1);
|
|
360
338
|
throw err;
|
|
361
339
|
}
|
|
362
340
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
341
|
+
if (!fileBasedEnabled && command.subCommands && rawArgv.length > 0 && !isFlag(rawArgv[0])) {
|
|
342
|
+
const [maybeSub, ...subCmdArgv] = rawArgv;
|
|
343
|
+
let subSpec;
|
|
344
|
+
for (const [key, spec] of Object.entries(command.subCommands)) {
|
|
345
|
+
if (key === maybeSub) {
|
|
346
|
+
subSpec = spec;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const cmd = await loadSubCommand(spec);
|
|
351
|
+
if (cmd.meta.aliases?.includes(maybeSub)) {
|
|
352
|
+
subSpec = spec;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
debugLog(`Error checking alias for subcommand ${key}:`, err);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (subSpec) {
|
|
360
|
+
try {
|
|
361
|
+
if (typeof command.onCmdStart === "function")
|
|
362
|
+
await command.onCmdStart();
|
|
363
|
+
await runSubCommand(
|
|
364
|
+
subSpec,
|
|
365
|
+
subCmdArgv,
|
|
366
|
+
parserOptions,
|
|
367
|
+
command.onCmdEnd
|
|
368
|
+
);
|
|
369
|
+
if (autoExit) process.exit(0);
|
|
370
|
+
return;
|
|
371
|
+
} catch (err) {
|
|
372
|
+
relinka("error", "Error running subcommand:", err.message);
|
|
373
|
+
if (autoExit) process.exit(1);
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
await runCommandWithArgs(command, rawArgv, parserOptions);
|
|
380
|
+
} finally {
|
|
381
|
+
}
|
|
382
|
+
await relinkaShutdown();
|
|
367
383
|
} finally {
|
|
368
|
-
if (typeof command.
|
|
384
|
+
if (typeof command.onLauncherEnd === "function")
|
|
385
|
+
await command.onLauncherEnd();
|
|
369
386
|
}
|
|
370
|
-
await relinkaShutdown();
|
|
371
387
|
}
|
|
372
388
|
function checkHelp(argv) {
|
|
373
389
|
return argv.includes("--help") || argv.includes("-h");
|
|
@@ -392,7 +408,7 @@ async function loadSubCommand(spec) {
|
|
|
392
408
|
}
|
|
393
409
|
throw new Error("Subcommand import did not return a valid command");
|
|
394
410
|
}
|
|
395
|
-
async function runFileBasedSubCmd(subName, argv, fileCmdOpts, parserOptions,
|
|
411
|
+
async function runFileBasedSubCmd(subName, argv, fileCmdOpts, parserOptions, parentFinish) {
|
|
396
412
|
const subPathDir = path.join(fileCmdOpts.cmdsRootPath, subName);
|
|
397
413
|
let importPath;
|
|
398
414
|
const possibleFiles = [
|
|
@@ -447,15 +463,15 @@ Info for this CLI's developer: No valid command directory found, expected: ${exp
|
|
|
447
463
|
try {
|
|
448
464
|
await runCommandWithArgs(subCommand, argv, parserOptions);
|
|
449
465
|
} finally {
|
|
450
|
-
if (typeof
|
|
466
|
+
if (typeof parentFinish === "function") await parentFinish();
|
|
451
467
|
}
|
|
452
468
|
}
|
|
453
|
-
async function runSubCommand(spec, argv, parserOptions,
|
|
469
|
+
async function runSubCommand(spec, argv, parserOptions, parentFinish) {
|
|
454
470
|
const subCommand = await loadSubCommand(spec);
|
|
455
471
|
try {
|
|
456
472
|
await runCommandWithArgs(subCommand, argv, parserOptions);
|
|
457
473
|
} finally {
|
|
458
|
-
if (typeof
|
|
474
|
+
if (typeof parentFinish === "function") await parentFinish();
|
|
459
475
|
}
|
|
460
476
|
}
|
|
461
477
|
async function runCommandWithArgs(command, argv, parserOptions) {
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"name": "@reliverse/rempts",
|
|
30
30
|
"type": "module",
|
|
31
|
-
"version": "1.7.
|
|
31
|
+
"version": "1.7.1",
|
|
32
32
|
"author": "reliverse",
|
|
33
33
|
"bugs": {
|
|
34
34
|
"email": "blefnk@gmail.com",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/bun": "^1.2.13",
|
|
52
52
|
"@types/figlet": "^1.7.0",
|
|
53
53
|
"@types/fs-extra": "^11.0.4",
|
|
54
|
-
"@types/node": "^22.15.
|
|
54
|
+
"@types/node": "^22.15.18",
|
|
55
55
|
"@types/terminal-kit": "^2.5.7",
|
|
56
56
|
"@types/wrap-ansi": "^8.1.0",
|
|
57
57
|
"eslint": "^9.26.0",
|