cli-kiss 0.2.4 → 0.2.6
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/dist/index.d.ts +159 -167
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +3 -2
- package/docs/.vitepress/theme/style.css +6 -2
- package/docs/guide/01_getting_started.md +12 -13
- package/docs/guide/02_commands.md +12 -29
- package/docs/guide/03_options.md +16 -25
- package/docs/guide/04_positionals.md +45 -55
- package/docs/guide/05_input_types.md +134 -0
- package/docs/guide/06_run_as_cli.md +143 -0
- package/docs/index.md +3 -2
- package/docs/public/favicon.ico +0 -0
- package/docs/public/hero.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +0 -2
- package/src/lib/Command.ts +14 -35
- package/src/lib/Operation.ts +13 -4
- package/src/lib/Option.ts +118 -162
- package/src/lib/Positional.ts +37 -62
- package/src/lib/Reader.ts +3 -3
- package/src/lib/Run.ts +76 -49
- package/src/lib/Type.ts +227 -143
- package/src/lib/Typo.ts +55 -23
- package/src/lib/Usage.ts +30 -45
- package/tests/unit.Reader.parsings.ts +50 -0
- package/tests/unit.command.execute.ts +13 -13
- package/tests/unit.command.usage.ts +60 -54
- package/tests/unit.runner.colors.ts +199 -0
- package/tests/unit.runner.cycle.ts +69 -55
- package/tests/unit.runner.errors.ts +12 -20
- package/docs/guide/05_types.md +0 -132
- package/docs/guide/06_run.md +0 -160
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Running your CLI
|
|
2
|
+
|
|
3
|
+
## `runAndExit`
|
|
4
|
+
|
|
5
|
+
`runAndExit` parses arguments, runs the matched command, and exits.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
await runAndExit(cliName, cliArgs, context, command, options?);
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Parameter | Type | Description |
|
|
12
|
+
| --------- | ------------------------ | ------------------------------------------------- |
|
|
13
|
+
| `cliName` | `string` | Program name used in help and `--version` output |
|
|
14
|
+
| `cliArgs` | `ReadonlyArray<string>` | Raw arguments — typically `process.argv.slice(2)` |
|
|
15
|
+
| `context` | `Context` | Value forwarded to every command handler |
|
|
16
|
+
| `command` | `Command<Context, void>` | The root command |
|
|
17
|
+
| `options` | `object?` | See below |
|
|
18
|
+
|
|
19
|
+
### Options
|
|
20
|
+
|
|
21
|
+
| Option | Type | Default | Description |
|
|
22
|
+
| -------------- | ----------------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `buildVersion` | `string?` | — | Enables `--version` flag; prints `<cliName> <buildVersion>` |
|
|
24
|
+
| `usageOnHelp` | `boolean?` | `true` | Enables `--help` flag |
|
|
25
|
+
| `usageOnError` | `boolean?` | `true` | Prints usage to stderr when parsing fails |
|
|
26
|
+
| `colorSetup` | `flag` / `env` / `always` / `never` | `"flag"` | Color mode: `"flag"` adds a `--color` option; `"env"` reads env vars; others force the mode |
|
|
27
|
+
| `onError` | `(error: unknown) => void` | — | Custom handler for parse and execution errors |
|
|
28
|
+
| `onExit` | `(code: number) => never` | `process.exit` | Override for testing |
|
|
29
|
+
|
|
30
|
+
### Exit codes
|
|
31
|
+
|
|
32
|
+
| Code | Reason |
|
|
33
|
+
| ---- | --------------------------------------- |
|
|
34
|
+
| `0` | Success, `--help`, or `--version` |
|
|
35
|
+
| `1` | Parse error or uncaught execution error |
|
|
36
|
+
|
|
37
|
+
## Full example
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
type Ctx = { db: string };
|
|
41
|
+
|
|
42
|
+
const deployCmd = command(
|
|
43
|
+
{ description: "Deploy to production" },
|
|
44
|
+
operation(
|
|
45
|
+
{
|
|
46
|
+
options: {
|
|
47
|
+
dryRun: optionFlag({ long: "dry-run", description: "Simulate only" }),
|
|
48
|
+
},
|
|
49
|
+
positionals: [],
|
|
50
|
+
},
|
|
51
|
+
async ({ db }, { options: { dryRun } }) => {
|
|
52
|
+
if (dryRun) {
|
|
53
|
+
console.log(`[dry-run] would deploy with DB: ${db}`);
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`Deploying with DB: ${db}`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const rootCmd = commandWithSubcommands(
|
|
62
|
+
{ description: "My deployment CLI" },
|
|
63
|
+
operation(
|
|
64
|
+
{
|
|
65
|
+
options: {
|
|
66
|
+
dbUrl: optionSingleValue({
|
|
67
|
+
long: "db",
|
|
68
|
+
type: typeUrl(),
|
|
69
|
+
description: "Database URL",
|
|
70
|
+
defaultWhenNotDefined: () => new URL("postgres://localhost/mydb"),
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
positionals: [],
|
|
74
|
+
},
|
|
75
|
+
async (_ctx, { options: { dbUrl } }): Promise<Ctx> => ({
|
|
76
|
+
db: dbUrl.toString(),
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
{ deploy: deployCmd },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await runAndExit("my-cli", process.argv.slice(2), undefined, rootCmd, {
|
|
83
|
+
buildVersion: "2.0.0",
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Check it
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
my-cli --help
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
Usage: my-cli <subcommand>
|
|
95
|
+
|
|
96
|
+
My deployment CLI
|
|
97
|
+
|
|
98
|
+
Subcommands:
|
|
99
|
+
deploy Deploy to production
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--db <url> Database URL
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Try it
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
my-cli deploy --dry-run
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
[dry-run] would deploy with DB: postgres://localhost/mydb
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Color control
|
|
116
|
+
|
|
117
|
+
Colors are auto-detected by default (`colorSetup: "flag"` adds a `--color`
|
|
118
|
+
option). Override:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// Read from env vars (FORCE_COLOR, NO_COLOR), same as `--color=auto`
|
|
122
|
+
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "env" });
|
|
123
|
+
// Force colors on
|
|
124
|
+
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "always" });
|
|
125
|
+
// Force colors off (useful in CI)
|
|
126
|
+
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "never" });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Testing your CLI
|
|
130
|
+
|
|
131
|
+
Override `onExit` to prevent process exit during tests:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const exitCodes: number[] = [];
|
|
135
|
+
await runAndExit("my-cli", ["--help"], undefined, myCommand, {
|
|
136
|
+
colorSetup: "never",
|
|
137
|
+
onExit: (code) => {
|
|
138
|
+
exitCodes.push(code);
|
|
139
|
+
return undefined as never;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
console.assert(exitCodes[0] === 0, "expected exit 0 for --help");
|
|
143
|
+
```
|
package/docs/index.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
layout: home
|
|
3
3
|
|
|
4
4
|
hero:
|
|
5
|
-
name: CLI-
|
|
5
|
+
name: CLI-Kiss
|
|
6
6
|
text: CLI for TypeScript.
|
|
7
7
|
|
|
8
8
|
tagline:
|
|
9
|
-
No bloat, no dependencies.<br/>Standard behavior users expect.<br/>Keep It
|
|
9
|
+
No bloat, no dependencies.<br/>Standard behavior users expect.<br/>Keep It
|
|
10
|
+
Simple and Stupid, it just does the job.
|
|
10
11
|
|
|
11
12
|
image:
|
|
12
13
|
src: /hero.png
|
|
Binary file
|
package/docs/public/hero.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/lib/Command.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
typoStyleUserInput,
|
|
8
8
|
TypoText,
|
|
9
9
|
} from "./Typo";
|
|
10
|
-
import { UsageCommand
|
|
10
|
+
import { UsageCommand } from "./Usage";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* A CLI command. Created with {@link command}, {@link commandWithSubcommands}, or {@link commandChained}.
|
|
@@ -93,8 +93,8 @@ export type CommandInformation = {
|
|
|
93
93
|
| { subcommand: string }
|
|
94
94
|
| {
|
|
95
95
|
option:
|
|
96
|
-
| { long: string;
|
|
97
|
-
| { short: string;
|
|
96
|
+
| { long: string; inlined?: string; separated?: Array<string> }
|
|
97
|
+
| { short: string; inlined?: string; separated?: Array<string> };
|
|
98
98
|
}
|
|
99
99
|
>;
|
|
100
100
|
}>;
|
|
@@ -115,7 +115,7 @@ export type CommandInformation = {
|
|
|
115
115
|
* const greet = command(
|
|
116
116
|
* { description: "Greet a user" },
|
|
117
117
|
* operation(
|
|
118
|
-
* { options: {}, positionals: [positionalRequired({ type:
|
|
118
|
+
* { options: {}, positionals: [positionalRequired({ type: type("name") })] },
|
|
119
119
|
* async (_ctx, { positionals: [name] }) => console.log(`Hello, ${name}!`),
|
|
120
120
|
* ),
|
|
121
121
|
* );
|
|
@@ -192,7 +192,7 @@ export function command<Context, Result>(
|
|
|
192
192
|
export function commandWithSubcommands<Context, Payload, Result>(
|
|
193
193
|
information: CommandInformation,
|
|
194
194
|
operation: Operation<Context, Payload>,
|
|
195
|
-
subcommands: { [subcommand:
|
|
195
|
+
subcommands: { [subcommand: string]: Command<Payload, Result> },
|
|
196
196
|
): Command<Context, Result> {
|
|
197
197
|
return {
|
|
198
198
|
getInformation() {
|
|
@@ -205,17 +205,16 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
205
205
|
if (subcommandName === undefined) {
|
|
206
206
|
throw new TypoError(
|
|
207
207
|
new TypoText(
|
|
208
|
-
new TypoString(`<
|
|
208
|
+
new TypoString(`<subcommand>`, typoStyleUserInput),
|
|
209
209
|
new TypoString(`: Is required, but was not provided`),
|
|
210
210
|
),
|
|
211
211
|
);
|
|
212
212
|
}
|
|
213
|
-
const subcommandInput =
|
|
214
|
-
subcommands[subcommandName as Lowercase<string>];
|
|
213
|
+
const subcommandInput = subcommands[subcommandName];
|
|
215
214
|
if (subcommandInput === undefined) {
|
|
216
215
|
throw new TypoError(
|
|
217
216
|
new TypoText(
|
|
218
|
-
new TypoString(`<
|
|
217
|
+
new TypoString(`<subcommand>`, typoStyleUserInput),
|
|
219
218
|
new TypoString(`: Invalid value: `),
|
|
220
219
|
new TypoString(`"${subcommandName}"`, typoStyleQuote),
|
|
221
220
|
),
|
|
@@ -227,7 +226,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
227
226
|
generateUsage() {
|
|
228
227
|
const subcommandUsage = subcommandDecoder.generateUsage();
|
|
229
228
|
const currentUsage = generateUsageLeaf(information, operation);
|
|
230
|
-
currentUsage.segments.push(
|
|
229
|
+
currentUsage.segments.push({ subcommand: subcommandName });
|
|
231
230
|
currentUsage.segments.push(...subcommandUsage.segments);
|
|
232
231
|
currentUsage.information = subcommandUsage.information;
|
|
233
232
|
currentUsage.positionals.push(...subcommandUsage.positionals);
|
|
@@ -253,7 +252,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
253
252
|
return {
|
|
254
253
|
generateUsage() {
|
|
255
254
|
const currentUsage = generateUsageLeaf(information, operation);
|
|
256
|
-
currentUsage.segments.push(
|
|
255
|
+
currentUsage.segments.push({ positional: "<subcommand>" });
|
|
257
256
|
for (const [name, subcommand] of Object.entries(subcommands)) {
|
|
258
257
|
const { description, hint } = subcommand.getInformation();
|
|
259
258
|
currentUsage.subcommands.push({ name, description, hint });
|
|
@@ -281,18 +280,6 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
281
280
|
* @param operation - Runs first; output becomes `subcommand`'s context.
|
|
282
281
|
* @param subcommand - Runs after `operation`.
|
|
283
282
|
* @returns A {@link Command} composing both stages.
|
|
284
|
-
*
|
|
285
|
-
* @example
|
|
286
|
-
* ```ts
|
|
287
|
-
* const authenticatedDeploy = commandChained(
|
|
288
|
-
* { description: "Authenticate then deploy" },
|
|
289
|
-
* operation(
|
|
290
|
-
* { options: { token: optionSingleValue({ long: "token", type: typeString, default: () => "" }) }, positionals: [] },
|
|
291
|
-
* async (_ctx, { options: { token } }) => ({ token }),
|
|
292
|
-
* ),
|
|
293
|
-
* command({ description: "Deploy" }, deployOperation),
|
|
294
|
-
* );
|
|
295
|
-
* ```
|
|
296
283
|
*/
|
|
297
284
|
export function commandChained<Context, Payload, Result>(
|
|
298
285
|
information: CommandInformation,
|
|
@@ -336,7 +323,7 @@ export function commandChained<Context, Payload, Result>(
|
|
|
336
323
|
return {
|
|
337
324
|
generateUsage() {
|
|
338
325
|
const currentUsage = generateUsageLeaf(information, operation);
|
|
339
|
-
currentUsage.segments.push(
|
|
326
|
+
currentUsage.segments.push({ positional: "[REST]..." });
|
|
340
327
|
return currentUsage;
|
|
341
328
|
},
|
|
342
329
|
decodeAndMakeInterpreter() {
|
|
@@ -348,23 +335,15 @@ export function commandChained<Context, Payload, Result>(
|
|
|
348
335
|
};
|
|
349
336
|
}
|
|
350
337
|
|
|
351
|
-
function segmentPositional(value: string): UsageSegment {
|
|
352
|
-
return { positional: value };
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function segmentSubcommand(value: string): UsageSegment {
|
|
356
|
-
return { subcommand: value };
|
|
357
|
-
}
|
|
358
|
-
|
|
359
338
|
function generateUsageLeaf(
|
|
360
339
|
information: CommandInformation,
|
|
361
340
|
operation: Operation<any, any>,
|
|
362
341
|
): UsageCommand {
|
|
363
342
|
const { positionals, options } = operation.generateUsage();
|
|
364
343
|
return {
|
|
365
|
-
segments: positionals.map((positional) =>
|
|
366
|
-
|
|
367
|
-
),
|
|
344
|
+
segments: positionals.map((positional) => ({
|
|
345
|
+
positional: positional.label,
|
|
346
|
+
})),
|
|
368
347
|
information,
|
|
369
348
|
positionals,
|
|
370
349
|
subcommands: [],
|
package/src/lib/Operation.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Option, OptionDecoder } from "./Option";
|
|
2
2
|
import { Positional, PositionalDecoder } from "./Positional";
|
|
3
3
|
import { ReaderArgs } from "./Reader";
|
|
4
|
-
import {
|
|
4
|
+
import { UsageOption, UsagePositional } from "./Usage";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Options, positionals, and an async handler that together form the logic of a CLI command.
|
|
@@ -16,7 +16,16 @@ export type Operation<Context, Result> = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Returns usage metadata without consuming any arguments.
|
|
18
18
|
*/
|
|
19
|
-
generateUsage():
|
|
19
|
+
generateUsage(): {
|
|
20
|
+
/**
|
|
21
|
+
* Registered options.
|
|
22
|
+
*/
|
|
23
|
+
options: Array<UsageOption>;
|
|
24
|
+
/**
|
|
25
|
+
* Declared positionals, in order.
|
|
26
|
+
*/
|
|
27
|
+
positionals: Array<UsagePositional>;
|
|
28
|
+
};
|
|
20
29
|
/**
|
|
21
30
|
* Consumes args from `readerArgs` and returns an {@link OperationDecoder}.
|
|
22
31
|
*/
|
|
@@ -74,10 +83,10 @@ export type OperationInterpreter<Context, Result> = {
|
|
|
74
83
|
* const greetOperation = operation(
|
|
75
84
|
* {
|
|
76
85
|
* options: {
|
|
77
|
-
* loud: optionFlag({ long: "loud", description: "Print in uppercase" }),
|
|
86
|
+
* loud: optionFlag({ long: "loud", description: "Print in uppercase", default: false }),
|
|
78
87
|
* },
|
|
79
88
|
* positionals: [
|
|
80
|
-
* positionalRequired({ type:
|
|
89
|
+
* positionalRequired({ type: type("name"), description: "Name to greet" }),
|
|
81
90
|
* ],
|
|
82
91
|
* },
|
|
83
92
|
* async function (_ctx, { options: { loud }, positionals: [name] }) {
|