@ls-stack/cli 0.1.0

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 ADDED
@@ -0,0 +1,532 @@
1
+ # @ls-stack/cli
2
+
3
+ A TypeScript library for building interactive command-line interfaces with type-safe prompts and ESC-to-cancel support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ls-stack/cli
9
+ # or
10
+ yarn add @ls-stack/cli
11
+ ```
12
+
13
+ **Requirements:** Node.js >= 21.5.0
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { cliInput } from '@ls-stack/cli';
19
+
20
+ const name = await cliInput.text('What is your name?');
21
+ const proceed = await cliInput.confirm('Continue?', { initial: true });
22
+
23
+ console.log(`Hello, ${name}!`);
24
+ ```
25
+
26
+ ## API Reference
27
+
28
+ ### Types
29
+
30
+ ```typescript
31
+ type ValidateFn = (
32
+ value: string,
33
+ ) => boolean | string | Promise<boolean | string>;
34
+
35
+ type SelectOption<T extends string> = {
36
+ value: T;
37
+ label?: string;
38
+ hint?: string;
39
+ };
40
+ ```
41
+
42
+ ### `cliInput.select()`
43
+
44
+ Single selection from a list of options.
45
+
46
+ ```typescript
47
+ const choice = await cliInput.select<'dev' | 'staging' | 'prod'>(
48
+ 'Select environment',
49
+ {
50
+ options: [
51
+ { value: 'dev', label: 'Development', hint: 'Local development server' },
52
+ { value: 'staging', label: 'Staging', hint: 'Pre-production testing' },
53
+ { value: 'prod', label: 'Production', hint: 'Live environment' },
54
+ ],
55
+ },
56
+ );
57
+ // choice: 'dev' | 'staging' | 'prod'
58
+ ```
59
+
60
+ **Parameters:**
61
+
62
+ | Name | Type | Description |
63
+ | ----------------- | ------------------- | --------------------------- |
64
+ | `title` | `string` | The prompt message |
65
+ | `options.options` | `SelectOption<T>[]` | Array of selectable options |
66
+
67
+ **Returns:** `Promise<T>` - The selected option's value
68
+
69
+ ### `cliInput.multipleSelect()`
70
+
71
+ Multi-select with checkboxes. Requires at least one selection.
72
+
73
+ ```typescript
74
+ const features = await cliInput.multipleSelect<'ts' | 'eslint' | 'prettier'>(
75
+ 'Select features to enable',
76
+ {
77
+ options: [
78
+ { value: 'ts', label: 'TypeScript' },
79
+ { value: 'eslint', label: 'ESLint', hint: 'Code linting' },
80
+ { value: 'prettier', label: 'Prettier', hint: 'Code formatting' },
81
+ ],
82
+ },
83
+ );
84
+ // features: ('ts' | 'eslint' | 'prettier')[]
85
+ ```
86
+
87
+ **Parameters:**
88
+
89
+ | Name | Type | Description |
90
+ | ----------------- | ------------------- | --------------------------- |
91
+ | `title` | `string` | The prompt message |
92
+ | `options.options` | `SelectOption<T>[]` | Array of selectable options |
93
+
94
+ **Returns:** `Promise<T[]>` - Array of selected values
95
+
96
+ ### `cliInput.text()`
97
+
98
+ Text input with optional validation.
99
+
100
+ ```typescript
101
+ const projectName = await cliInput.text('Enter project name', {
102
+ initial: 'my-project',
103
+ validate: (value) => {
104
+ if (!/^[a-z0-9-]+$/.test(value)) {
105
+ return 'Only lowercase letters, numbers, and hyphens allowed';
106
+ }
107
+ return true;
108
+ },
109
+ });
110
+ ```
111
+
112
+ **Parameters:**
113
+
114
+ | Name | Type | Description |
115
+ | ------------------ | ------------- | ------------------- |
116
+ | `title` | `string` | The prompt message |
117
+ | `options.initial` | `string?` | Default value |
118
+ | `options.validate` | `ValidateFn?` | Validation function |
119
+
120
+ **Returns:** `Promise<string>` - The entered text
121
+
122
+ ### `cliInput.textWithAutocomplete()`
123
+
124
+ Text input with autocomplete suggestions. Searches across value, label, and hint.
125
+
126
+ ```typescript
127
+ const framework = await cliInput.textWithAutocomplete<
128
+ 'react' | 'vue' | 'svelte'
129
+ >('Select a framework', {
130
+ options: [
131
+ {
132
+ value: 'react',
133
+ label: 'React',
134
+ hint: 'A JavaScript library for building UIs',
135
+ },
136
+ {
137
+ value: 'vue',
138
+ label: 'Vue',
139
+ hint: 'The progressive JavaScript framework',
140
+ },
141
+ {
142
+ value: 'svelte',
143
+ label: 'Svelte',
144
+ hint: 'Cybernetically enhanced web apps',
145
+ },
146
+ ],
147
+ validate: (value) => value.length > 0 || 'Please select a framework',
148
+ });
149
+ ```
150
+
151
+ **Parameters:**
152
+
153
+ | Name | Type | Description |
154
+ | ------------------ | ------------------- | --------------------------------- |
155
+ | `title` | `string` | The prompt message |
156
+ | `options.options` | `SelectOption<T>[]` | Array of autocomplete suggestions |
157
+ | `options.validate` | `ValidateFn?` | Validation function |
158
+
159
+ **Returns:** `Promise<T>` - The selected or entered value
160
+
161
+ ### `cliInput.confirm()`
162
+
163
+ Yes/No boolean prompt.
164
+
165
+ ```typescript
166
+ const shouldDeploy = await cliInput.confirm('Deploy to production?', {
167
+ initial: false,
168
+ });
169
+ // shouldDeploy: boolean
170
+ ```
171
+
172
+ **Parameters:**
173
+
174
+ | Name | Type | Description |
175
+ | ----------------- | ---------- | ---------------------------------------------- |
176
+ | `title` | `string` | The prompt message |
177
+ | `options.initial` | `boolean?` | Default value (`true` for yes, `false` for no) |
178
+
179
+ **Returns:** `Promise<boolean>` - The user's choice
180
+
181
+ ### `cliInput.number()`
182
+
183
+ Numeric input.
184
+
185
+ ```typescript
186
+ const port = await cliInput.number('Enter port number', {
187
+ initial: 3000,
188
+ });
189
+ // port: number | null
190
+ ```
191
+
192
+ **Parameters:**
193
+
194
+ | Name | Type | Description |
195
+ | ----------------- | --------- | ------------------ |
196
+ | `title` | `string` | The prompt message |
197
+ | `options.initial` | `number?` | Default value |
198
+
199
+ **Returns:** `Promise<number | null>` - The entered number, or `null` on error
200
+
201
+ ---
202
+
203
+ ## CLI Framework
204
+
205
+ Build complete CLI applications with typed commands, automatic help generation, and interactive mode.
206
+
207
+ ### Quick Start
208
+
209
+ ```typescript
210
+ import { createCLI, createCmd } from '@ls-stack/cli';
211
+
212
+ await createCLI(
213
+ { name: 'My CLI', baseCmd: 'my-cli' },
214
+ {
215
+ hello: createCmd({
216
+ short: 'hi',
217
+ description: 'Say hello',
218
+ run: async () => {
219
+ console.log('Hello, World!');
220
+ },
221
+ }),
222
+ },
223
+ );
224
+ ```
225
+
226
+ ### Argument Types
227
+
228
+ ```typescript
229
+ type Arg =
230
+ | { type: 'positional-string'; name: string; description: string; default?: string }
231
+ | { type: 'positional-number'; name: string; description: string; default?: number }
232
+ | { type: 'flag'; name: string; description: string }
233
+ | { type: 'value-string-flag'; name: string; description: string; default?: string }
234
+ | { type: 'value-number-flag'; name: string; description: string; default?: number };
235
+ ```
236
+
237
+ | Type | CLI Usage | TypeScript Type |
238
+ | -------------------- | ---------------------- | ------------------------------------------------- |
239
+ | `positional-string` | `my-cli cmd value` | `string` (or `string \| undefined` if no default) |
240
+ | `positional-number` | `my-cli cmd 42` | `number` (or `number \| undefined` if no default) |
241
+ | `flag` | `my-cli cmd --verbose` | `boolean` (defaults to `false`) |
242
+ | `value-string-flag` | `my-cli cmd --env dev` | `string \| undefined` (or `string` if default) |
243
+ | `value-number-flag` | `my-cli cmd --port 80` | `number \| undefined` (or `number` if default) |
244
+
245
+ ### `createCmd()`
246
+
247
+ Creates a type-safe command definition.
248
+
249
+ ```typescript
250
+ const deploy = createCmd({
251
+ short: 'd',
252
+ description: 'Deploy the application',
253
+ args: {
254
+ env: {
255
+ type: 'positional-string',
256
+ name: 'env',
257
+ description: 'Target environment',
258
+ },
259
+ port: {
260
+ type: 'value-number-flag',
261
+ name: 'port',
262
+ description: 'Port number',
263
+ default: 3000,
264
+ },
265
+ verbose: {
266
+ type: 'flag',
267
+ name: 'verbose',
268
+ description: 'Enable verbose logging',
269
+ },
270
+ },
271
+ examples: [
272
+ { args: ['production'], description: 'Deploy to production' },
273
+ { args: ['staging', '--port', '8080'], description: 'Deploy to staging on port 8080' },
274
+ ],
275
+ run: async ({ env, port, verbose }) => {
276
+ // Types are inferred: env: string, port: number, verbose: boolean
277
+ console.log(`Deploying to ${env} on port ${port}`);
278
+ if (verbose) console.log('Verbose mode enabled');
279
+ },
280
+ });
281
+ ```
282
+
283
+ **Parameters:**
284
+
285
+ | Name | Type | Description |
286
+ | ------------- | ----------------------------- | ---------------------------------------------- |
287
+ | `description` | `string` | Command description shown in help |
288
+ | `short` | `string?` | Single-character alias (cannot be 'i' or 'h') |
289
+ | `args` | `Record<string, Arg>?` | Typed argument definitions |
290
+ | `run` | `(args) => void \| Promise` | Handler function receiving parsed arguments |
291
+ | `examples` | `{ args, description }[]?` | Usage examples for help text |
292
+
293
+ ### `createCLI()`
294
+
295
+ Creates and runs a CLI application.
296
+
297
+ ```typescript
298
+ await createCLI(
299
+ {
300
+ name: 'My CLI',
301
+ baseCmd: 'my-cli',
302
+ sort: ['deploy', 'build', 'test'], // Optional: custom command order
303
+ },
304
+ {
305
+ deploy: deployCmd,
306
+ build: buildCmd,
307
+ test: testCmd,
308
+ },
309
+ );
310
+ ```
311
+
312
+ **Parameters:**
313
+
314
+ | Name | Type | Description |
315
+ | -------------- | ----------------- | ------------------------------------- |
316
+ | `name` | `string` | CLI display name shown in header |
317
+ | `baseCmd` | `string` | Command prefix for help text |
318
+ | `sort` | `string[]?` | Custom command display order |
319
+ | `cmds` | `Record<C, Cmd>` | Commands created with `createCmd` |
320
+
321
+ ### Built-in Commands
322
+
323
+ | Command | Description |
324
+ | ------------------ | ---------------------------------------- |
325
+ | `h`, `--help` | Show help with all commands |
326
+ | `i` | Interactive mode (select from list) |
327
+ | `<command> -h` | Show help for a specific command |
328
+
329
+ ### CLI Usage Examples
330
+
331
+ ```bash
332
+ my-cli # Show interactive menu
333
+ my-cli h # Show help
334
+ my-cli --help # Show help
335
+ my-cli i # Interactive mode
336
+ my-cli deploy prod # Run deploy with positional arg
337
+ my-cli d prod # Run deploy via short alias
338
+ my-cli deploy -h # Show deploy command help
339
+ my-cli deploy prod --port 8080 --verbose
340
+ ```
341
+
342
+ ### Complete Example
343
+
344
+ ```typescript
345
+ import { createCLI, createCmd } from '@ls-stack/cli';
346
+
347
+ await createCLI(
348
+ { name: 'Project CLI', baseCmd: 'project' },
349
+ {
350
+ create: createCmd({
351
+ short: 'c',
352
+ description: 'Create a new project',
353
+ args: {
354
+ name: {
355
+ type: 'positional-string',
356
+ name: 'name',
357
+ description: 'Project name',
358
+ },
359
+ template: {
360
+ type: 'value-string-flag',
361
+ name: 'template',
362
+ description: 'Project template',
363
+ default: 'basic',
364
+ },
365
+ },
366
+ examples: [
367
+ { args: ['my-app'], description: 'Create with default template' },
368
+ { args: ['my-app', '--template', 'react'], description: 'Create React project' },
369
+ ],
370
+ run: async ({ name, template }) => {
371
+ console.log(`Creating ${name} with template: ${template}`);
372
+ },
373
+ }),
374
+
375
+ build: createCmd({
376
+ short: 'b',
377
+ description: 'Build the project',
378
+ args: {
379
+ watch: {
380
+ type: 'flag',
381
+ name: 'watch',
382
+ description: 'Watch for changes',
383
+ },
384
+ },
385
+ run: async ({ watch }) => {
386
+ console.log(watch ? 'Building in watch mode...' : 'Building...');
387
+ },
388
+ }),
389
+
390
+ serve: createCmd({
391
+ short: 's',
392
+ description: 'Start development server',
393
+ args: {
394
+ port: {
395
+ type: 'value-number-flag',
396
+ name: 'port',
397
+ description: 'Port number',
398
+ default: 3000,
399
+ },
400
+ },
401
+ run: async ({ port }) => {
402
+ console.log(`Server running on http://localhost:${port}`);
403
+ },
404
+ }),
405
+ },
406
+ );
407
+ ```
408
+
409
+ ## Features
410
+
411
+ ### ESC-to-Cancel
412
+
413
+ All prompts support pressing ESC to cancel. When cancelled, the process exits cleanly with code 0.
414
+
415
+ ### Type Safety
416
+
417
+ All prompts are fully typed. When using generic type parameters with `select`, `multipleSelect`, or `textWithAutocomplete`, the return type is narrowed to the union of option values.
418
+
419
+ ```typescript
420
+ // Return type is automatically 'small' | 'medium' | 'large'
421
+ const size = await cliInput.select<'small' | 'medium' | 'large'>(
422
+ 'Select size',
423
+ {
424
+ options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
425
+ },
426
+ );
427
+ ```
428
+
429
+ ### Validation
430
+
431
+ Text inputs support synchronous or asynchronous validation:
432
+
433
+ ```typescript
434
+ const email = await cliInput.text('Enter email', {
435
+ validate: async (value) => {
436
+ if (!value.includes('@')) {
437
+ return 'Invalid email format';
438
+ }
439
+ const exists = await checkEmailExists(value);
440
+ if (exists) {
441
+ return 'Email already registered';
442
+ }
443
+ return true;
444
+ },
445
+ });
446
+ ```
447
+
448
+ ## Examples
449
+
450
+ ### Interactive Setup Wizard
451
+
452
+ ```typescript
453
+ import { cliInput } from '@ls-stack/cli';
454
+
455
+ async function setupWizard() {
456
+ const projectName = await cliInput.text('Project name', {
457
+ validate: (v) => v.length >= 3 || 'Name must be at least 3 characters',
458
+ });
459
+
460
+ const template = await cliInput.select('Select template', {
461
+ options: [
462
+ { value: 'blank', label: 'Blank', hint: 'Empty project' },
463
+ { value: 'react', label: 'React', hint: 'React with Vite' },
464
+ { value: 'next', label: 'Next.js', hint: 'Full-stack React' },
465
+ ],
466
+ });
467
+
468
+ const features = await cliInput.multipleSelect('Enable features', {
469
+ options: [
470
+ { value: 'typescript', label: 'TypeScript' },
471
+ { value: 'eslint', label: 'ESLint' },
472
+ { value: 'prettier', label: 'Prettier' },
473
+ { value: 'testing', label: 'Testing (Vitest)' },
474
+ ],
475
+ });
476
+
477
+ const installDeps = await cliInput.confirm('Install dependencies?', {
478
+ initial: true,
479
+ });
480
+
481
+ return { projectName, template, features, installDeps };
482
+ }
483
+ ```
484
+
485
+ ### Configuration Menu
486
+
487
+ ```typescript
488
+ import { cliInput } from '@ls-stack/cli';
489
+
490
+ async function configMenu() {
491
+ const action = await cliInput.select('What would you like to configure?', {
492
+ options: [
493
+ { value: 'port', label: 'Server Port' },
494
+ { value: 'host', label: 'Host Address' },
495
+ { value: 'timeout', label: 'Request Timeout' },
496
+ ],
497
+ });
498
+
499
+ switch (action) {
500
+ case 'port': {
501
+ const port = await cliInput.number('Enter port', { initial: 3000 });
502
+ console.log(`Port set to ${port}`);
503
+ break;
504
+ }
505
+ case 'host': {
506
+ const host = await cliInput.text('Enter host', { initial: 'localhost' });
507
+ console.log(`Host set to ${host}`);
508
+ break;
509
+ }
510
+ case 'timeout': {
511
+ const timeout = await cliInput.number('Timeout (seconds)', {
512
+ initial: 30,
513
+ });
514
+ console.log(`Timeout set to ${timeout}s`);
515
+ break;
516
+ }
517
+ }
518
+ }
519
+ ```
520
+
521
+ ## Error Handling
522
+
523
+ All prompts handle errors gracefully:
524
+
525
+ - **User cancellation (ESC/Ctrl+C):** Process exits with code 0
526
+ - **Other errors:** Error is logged and process exits with code 1
527
+
528
+ For the `number()` prompt specifically, non-cancellation errors return `null` instead of exiting.
529
+
530
+ ## License
531
+
532
+ MIT