@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 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
- ## Stop Fighting Your CLI
7
+ ## Features
8
8
 
9
- - CLIs are still the sharpest tool in the builder's belt. But most libraries are either clunky or crusty.
10
- - **@reliverse/rempts** is your end-to-end CLI UI + command framework β€” made for developer experience, DX precision, and high-context terminal UX.
11
- - No more hacking together `inquirer`, `citty`, `commander`, `chalk`, and other friends.
12
-
13
- ## What Makes It Different?
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
- - 🚨 Crash-safe (Ctrl+C, SIGINT, errors)
24
- - πŸ†• Automatic commands creation via `rempts init --cmd my-cool-cmd`
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
- ![Rempts Example CLI Screenshot](./example.png)
37
+ ![Rempts Example CLI Screenshot](./example/example.png)
29
38
 
30
- ## Terminology
39
+ ## API Overview
31
40
 
32
- - **Command/Subcommand**: A command is a function that defines the behavior of a CLI.
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
- ## CLI Launcher (Router)
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
- 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:
59
+ > See [`src/mod.ts`](./src/mod.ts) for the full list of exports.
40
60
 
41
- - **File-Based & Defined Subcommands:**
42
- Use `subCommands` in your command definition or let the launcher automatically load commands from a specified directory.
61
+ ## Prompts
43
62
 
44
- - **Automatic Command Detection:**
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
- - **Built-In Flag Handling:**
52
- Automatically processes global flags such as:
53
- - `--help` and `-h` to show usage details.
54
- - `--version` and `-v` to display version information.
55
- - `--debug` for verbose logging during development.
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
- - **Unified Argument Parsing:**
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
- - **Customizable Behavior:**
61
- 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.
83
+ | Hook | Description |
84
+ |--------------|--------------------|
85
+ | `useSpinner` | Start/stop spinner |
62
86
 
63
- - **Error Management & Usage Output:**
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
- - **Lifecycle Hooks:**
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
- - **Dynamic Usage Examples:**
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
- - **File-Based & Programmatic Subcommands:**
73
- - Both file-based and object subcommands are fully supported. The launcher can introspect their argument definitions and metadata for help, usage, and validation.
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
- - **Context-Aware Help Output:**
77
- - 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.
96
+ import {
97
+ startPrompt,
98
+ inputPrompt,
99
+ selectPrompt,
100
+ defineCommand,
101
+ runMain
102
+ } from "@reliverse/rempts";
78
103
 
79
- - **Error Handling:**
80
- - 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.
104
+ async function main() {
105
+ await startPrompt({ title: "Project Setup" });
81
106
 
82
- - **Unified Argument Parsing:**
83
- - All arguments (positional, named, boolean, string, number, array) are parsed and validated automatically. Negated flags (like `--no-flag`) are supported out of the box.
107
+ const name = await inputPrompt({
108
+ title: "What's your project name?",
109
+ defaultValue: "my-cool-project",
110
+ });
84
111
 
85
- - **Extensible & Flexible:**
86
- - 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.
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
- - **Bun & Node.js Support:**
89
- - The launcher is designed to work in both Bun and Node.js environments, so you can use it in any modern JavaScript/TypeScript project.
122
+ console.log("Your result:", { name, framework });
123
+ };
90
124
 
91
- - **Prompt-First, Modern UX:**
92
- - The launcher integrates tightly with the prompt engine, so you can build interactive, delightful CLIs with minimal effort.
125
+ await main();
126
+ ```
93
127
 
94
- ### Launcher Usage Example
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
- setup() {
153
+ onCmdStart() {
110
154
  relinka("success", "Setup");
111
155
  },
112
- cleanup() {
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: "./cli/args", // default is `./app`
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
- ### File-Based Subcommands
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 i -g @reliverse/rempts-cli`
173
- - Use `rempts init --cmd my-cmd-1 my-cmd-2` to init commands automatically
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
- ## πŸ“¦ Built-In Prompts
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
- ## Theming + Customization
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
- ## Playground
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
- ## Usage Examples
268
+ ### Launcher Usage Examples
237
269
 
238
- ### Minimal Usage Example
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
- ### Medium Usage Example
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
- ### Classic Usage Example
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: "πŸš€ Project Setup",
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
- ### Advanced Usage Example
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: "πŸš€ Project Setup",
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
- setup: options.setup,
66
- cleanup: options.cleanup
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 (!parserOptions.fileBasedCmds && !command.subCommands) {
232
- let callerDir = process.cwd();
233
- let callerFile;
234
- try {
235
- const err = new Error();
236
- const stack = err.stack?.split("\n");
237
- if (stack) {
238
- for (const line of stack) {
239
- const match = /\((.*):(\d+):(\d+)\)/.exec(line) || /at (.*):(\d+):(\d+)/.exec(line);
240
- if (match?.[1] && !match[1].includes("launcher-mod")) {
241
- callerFile = match[1];
242
- break;
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
- if (callerFile) {
247
- callerDir = path.dirname(callerFile);
248
- const rel = path.relative(process.cwd(), callerFile);
249
- if (/app[/][^/]+[/]cmd\.(ts|js)$/.test(rel)) {
250
- relinka(
251
- "error",
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
- process.exit(1);
257
- }
258
- const mainEntry = process.argv[1] ? path.resolve(process.argv[1]) : void 0;
259
- if (mainEntry && path.resolve(callerFile) !== mainEntry) {
260
- relinka(
261
- "error",
262
- `runMain() should only be called from your main CLI entry file.
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
- process.exit(1);
278
+ );
279
+ process.exit(1);
280
+ }
268
281
  }
282
+ } catch (_e) {
269
283
  }
270
- } catch (_e) {
284
+ const defaultCmdsRoot = path.resolve(callerDir, "app");
285
+ parserOptions.fileBasedCmds = {
286
+ enable: true,
287
+ cmdsRootPath: defaultCmdsRoot
288
+ };
271
289
  }
272
- const defaultCmdsRoot = path.resolve(callerDir, "app");
273
- parserOptions.fileBasedCmds = {
274
- enable: true,
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
- "info",
302
- `${command.meta?.name} ${command.meta?.version ? `v${command.meta?.version}` : ""}`
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 (autoExit) process.exit(0);
306
- return;
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
- if (!fileBasedEnabled && command.subCommands && rawArgv.length > 0 && !isFlag(rawArgv[0])) {
329
- const [maybeSub, ...subCmdArgv] = rawArgv;
330
- let subSpec;
331
- for (const [key, spec] of Object.entries(command.subCommands)) {
332
- if (key === maybeSub) {
333
- subSpec = spec;
334
- break;
335
- }
336
- try {
337
- const cmd = await loadSubCommand(spec);
338
- if (cmd.meta.aliases?.includes(maybeSub)) {
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
- if (subSpec) {
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.setup === "function") await command.setup();
349
- await runSubCommand(
350
- subSpec,
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.cleanup
331
+ command.onCmdEnd
354
332
  );
355
333
  if (autoExit) process.exit(0);
356
334
  return;
357
335
  } catch (err) {
358
- relinka("error", "Error running subcommand:", err.message);
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
- if (typeof command.setup === "function") await command.setup();
365
- try {
366
- await runCommandWithArgs(command, rawArgv, parserOptions);
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.cleanup === "function") await command.cleanup();
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, parentCleanup) {
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 parentCleanup === "function") await parentCleanup();
466
+ if (typeof parentFinish === "function") await parentFinish();
451
467
  }
452
468
  }
453
- async function runSubCommand(spec, argv, parserOptions, parentCleanup) {
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 parentCleanup === "function") await parentCleanup();
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.0",
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.17",
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",