cli-kiss 0.2.7 → 0.2.9
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 +8 -3
- package/dist/index.d.ts +200 -190
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -1
- package/docs/.vitepress/theme/Layout.vue +16 -0
- package/docs/.vitepress/theme/index.ts +5 -1
- package/docs/.vitepress/theme/style.css +5 -1
- package/docs/guide/01_getting_started.md +2 -2
- package/docs/guide/02_commands.md +3 -3
- package/docs/guide/03_options.md +11 -11
- package/docs/guide/04_positionals.md +9 -9
- package/docs/guide/05_input_types.md +17 -16
- package/docs/guide/06_run_as_cli.md +1 -1
- package/docs/index.md +2 -2
- package/docs/public/favicon.ico +0 -0
- package/docs/public/logo.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/lib/Command.ts +51 -40
- package/src/lib/Operation.ts +41 -25
- package/src/lib/Option.ts +198 -127
- package/src/lib/Positional.ts +51 -25
- package/src/lib/Reader.ts +188 -226
- package/src/lib/Run.ts +20 -9
- package/src/lib/Suggest.ts +78 -0
- package/src/lib/Type.ts +178 -154
- package/src/lib/Typo.ts +58 -55
- package/src/lib/Usage.ts +12 -12
- package/tests/unit.Reader.commons.ts +86 -123
- package/tests/unit.Reader.parsings.ts +14 -26
- package/tests/unit.Reader.shortBig.ts +75 -101
- package/tests/unit.command.aliases.ts +88 -0
- package/tests/unit.command.execute.ts +6 -6
- package/tests/unit.command.usage.ts +19 -13
- package/tests/unit.fuzzed.alternatives.ts +35 -26
- package/tests/unit.runner.colors.ts +8 -33
- package/tests/unit.runner.cycle.ts +141 -156
- package/tests/unit.runner.errors.ts +25 -22
- package/docs/public/hero.png +0 -0
- package/src/lib/Similarity.ts +0 -41
- package/tests/unit.Reader.aliases.ts +0 -62
|
@@ -2,7 +2,7 @@ import { defineConfig } from "vitepress";
|
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
description: "Full-featured TypeScript CLI builder. No bloat, no dependency.",
|
|
5
|
-
title: "cli-kiss
|
|
5
|
+
title: "cli-kiss",
|
|
6
6
|
base: "/cli-kiss/",
|
|
7
7
|
head: [
|
|
8
8
|
["link", { rel: "icon", href: "/cli-kiss/favicon.ico" }],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import DefaultTheme from 'vitepress/theme'
|
|
3
|
+
const { Layout } = DefaultTheme
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<Layout>
|
|
8
|
+
<template #nav-bar-title-before>
|
|
9
|
+
<img
|
|
10
|
+
src="/logo.png"
|
|
11
|
+
alt="logo"
|
|
12
|
+
style="width:32px;height:32px;margin-right:8px;display:block;"
|
|
13
|
+
>
|
|
14
|
+
</template>
|
|
15
|
+
</Layout>
|
|
16
|
+
</template>
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
--vp-home-hero-name-color: transparent;
|
|
4
4
|
--vp-home-hero-name-background: linear-gradient(-50deg, #ff003caa 30%, #459900aa 70%);
|
|
5
5
|
*/
|
|
6
|
-
--vp-home-hero-image-background-image: linear-gradient(
|
|
6
|
+
--vp-home-hero-image-background-image: linear-gradient(
|
|
7
|
+
-50deg,
|
|
8
|
+
#ff003c88 25%,
|
|
9
|
+
#008732aa 60%
|
|
10
|
+
);
|
|
7
11
|
--vp-home-hero-image-filter: blur(60px);
|
|
8
12
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
optionFlag,
|
|
21
21
|
positionalRequired,
|
|
22
22
|
runAndExit,
|
|
23
|
-
|
|
23
|
+
typeString,
|
|
24
24
|
} from "cli-kiss";
|
|
25
25
|
|
|
26
26
|
const greetCommand = command(
|
|
@@ -32,7 +32,7 @@ const greetCommand = command(
|
|
|
32
32
|
},
|
|
33
33
|
positionals: [
|
|
34
34
|
positionalRequired({
|
|
35
|
-
type:
|
|
35
|
+
type: typeString("name"),
|
|
36
36
|
description: "The name of the person to greet",
|
|
37
37
|
}),
|
|
38
38
|
],
|
|
@@ -12,7 +12,7 @@ const greet = command(
|
|
|
12
12
|
operation(
|
|
13
13
|
{
|
|
14
14
|
options: {},
|
|
15
|
-
positionals: [positionalRequired({ type:
|
|
15
|
+
positionals: [positionalRequired({ type: typeString("name") })],
|
|
16
16
|
},
|
|
17
17
|
async function (_ctx, { positionals: [name] }) {
|
|
18
18
|
console.log(`Hello, ${name}!`);
|
|
@@ -85,9 +85,9 @@ const authenticatedDeploy = commandChained(
|
|
|
85
85
|
options: {
|
|
86
86
|
token: optionSingleValue({
|
|
87
87
|
long: "token",
|
|
88
|
-
type:
|
|
88
|
+
type: typeString("secret"),
|
|
89
89
|
description: "API token",
|
|
90
|
-
|
|
90
|
+
fallbackValueIfAbsent: function () {
|
|
91
91
|
const t = process.env.API_TOKEN;
|
|
92
92
|
if (!t) throw new Error("API_TOKEN env var is required");
|
|
93
93
|
return t;
|
package/docs/guide/03_options.md
CHANGED
|
@@ -44,7 +44,7 @@ const output = optionSingleValue({
|
|
|
44
44
|
short: "o",
|
|
45
45
|
type: typePath(),
|
|
46
46
|
description: "Output directory",
|
|
47
|
-
|
|
47
|
+
fallbackValueIfAbsent: () => "dist/",
|
|
48
48
|
});
|
|
49
49
|
// --output dist/ → "dist/"
|
|
50
50
|
// --output=dist/ → "dist/"
|
|
@@ -52,16 +52,16 @@ const output = optionSingleValue({
|
|
|
52
52
|
// (absent) → "dist/"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
| Parameter
|
|
56
|
-
|
|
|
57
|
-
| `long`
|
|
58
|
-
| `short`
|
|
59
|
-
| `type`
|
|
60
|
-
| `description`
|
|
61
|
-
| `hint`
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `aliases`
|
|
55
|
+
| Parameter | Type | Description |
|
|
56
|
+
| -------------------------- | --------------------- | ---------------------------------------------------------------------------- |
|
|
57
|
+
| `long` | `string` | Long option name |
|
|
58
|
+
| `short` | `string?` | Short option name |
|
|
59
|
+
| `type` | `Type<Value>` | Decoder for the value |
|
|
60
|
+
| `description` | `string?` | Help text |
|
|
61
|
+
| `hint` | `string?` | Short note in parentheses |
|
|
62
|
+
| `fallbackValueIfAbsent` | `() => Value` | Value when option is absent — **throw** to make it required |
|
|
63
|
+
| `impliedValueIfNotInlined` | `() => Value?` | Value when option is present but has no inline value (e.g. `--output` alone) |
|
|
64
|
+
| `aliases` | `{ longs?, shorts? }` | Additional names |
|
|
65
65
|
|
|
66
66
|
## `optionRepeatable` — collect multiple values
|
|
67
67
|
|
|
@@ -9,7 +9,7 @@ Fails if missing.
|
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
11
|
const name = positionalRequired({
|
|
12
|
-
type:
|
|
12
|
+
type: typeString("person"),
|
|
13
13
|
description: "The name of the person to greet",
|
|
14
14
|
});
|
|
15
15
|
// Usage:
|
|
@@ -29,7 +29,7 @@ Falls back to a default when absent.
|
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
31
|
const greeting = positionalOptional({
|
|
32
|
-
type:
|
|
32
|
+
type: typeString("greeting"),
|
|
33
33
|
description: "Custom greeting",
|
|
34
34
|
hint: "default to 'Hello'",
|
|
35
35
|
default: () => "Hello",
|
|
@@ -66,7 +66,7 @@ Optionally stop collecting at a specific sentinel token:
|
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
68
|
const args = positionalVariadics({
|
|
69
|
-
type:
|
|
69
|
+
type: typeString("argument"),
|
|
70
70
|
endDelimiter: "STOP",
|
|
71
71
|
description: "Arguments (end with STOP)",
|
|
72
72
|
});
|
|
@@ -90,10 +90,10 @@ operation(
|
|
|
90
90
|
{
|
|
91
91
|
options: {},
|
|
92
92
|
positionals: [
|
|
93
|
-
positionalRequired({ type:
|
|
94
|
-
positionalRequired({ type:
|
|
95
|
-
positionalOptional({ type:
|
|
96
|
-
positionalVariadics({ type:
|
|
93
|
+
positionalRequired({ type: typeString("src") }),
|
|
94
|
+
positionalRequired({ type: typeString("dst") }),
|
|
95
|
+
positionalOptional({ type: typeString("tag"), default: () => "latest" }),
|
|
96
|
+
positionalVariadics({ type: typeString("extra") }),
|
|
97
97
|
],
|
|
98
98
|
},
|
|
99
99
|
async function (_ctx, { positionals: [src, dst, tag, extras] }) {
|
|
@@ -101,6 +101,6 @@ operation(
|
|
|
101
101
|
},
|
|
102
102
|
);
|
|
103
103
|
// Usage:
|
|
104
|
-
// my-cli in out → src="in",
|
|
105
|
-
// my-cli in out v2 a b c → src="in",
|
|
104
|
+
// my-cli in out → src="in", dst="out", tag="latest", extras=[]
|
|
105
|
+
// my-cli in out v2 a b c → src="in", dst="out", tag="v2", extras=["a","b","c"]
|
|
106
106
|
```
|
|
@@ -14,7 +14,7 @@ shown in help/errors.
|
|
|
14
14
|
|
|
15
15
|
| Type factory | Content type | Accepts |
|
|
16
16
|
| -------------- | ------------ | ------------------------------------------------------------------- |
|
|
17
|
-
| `
|
|
17
|
+
| `typeString` | `string` | Any string |
|
|
18
18
|
| `typeBoolean` | `boolean` | `true/yes/on/y` → true, `false/no/off/n` → false (case-insensitive) |
|
|
19
19
|
| `typeNumber` | `number` | Integers, floats, scientific notation |
|
|
20
20
|
| `typeInteger` | `bigint` | Integer strings only |
|
|
@@ -23,7 +23,7 @@ shown in help/errors.
|
|
|
23
23
|
| `typePath` | `string` | Non-empty path strings; optional sync existence check |
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
|
|
26
|
+
typeString("greeting").decoder("hello"); // → "hello"
|
|
27
27
|
typeBoolean("flag").decoder("yes"); // → true
|
|
28
28
|
typeNumber("pi").decoder("3.14"); // → 3.14
|
|
29
29
|
typeInteger("id").decoder("9007199254740993"); // → 9007199254740993n
|
|
@@ -47,8 +47,7 @@ Accepts only a fixed set of strings (case-insensitive by default):
|
|
|
47
47
|
const typeEnv = typeChoice("environment", ["dev", "staging", "prod"]);
|
|
48
48
|
typeEnv.decoder("prod"); // → "prod"
|
|
49
49
|
typeEnv.decoder("PROD"); // → "prod" (case-insensitive)
|
|
50
|
-
typeEnv.decoder("unknown");
|
|
51
|
-
// Error: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
|
|
50
|
+
typeEnv.decoder("unknown"); // Error: Invalid value: "unknown"
|
|
52
51
|
```
|
|
53
52
|
|
|
54
53
|
Pass `true` as third argument to make matching case-sensitive.
|
|
@@ -58,15 +57,15 @@ Pass `true` as third argument to make matching case-sensitive.
|
|
|
58
57
|
Splits a string into a fixed-length typed tuple:
|
|
59
58
|
|
|
60
59
|
```ts
|
|
61
|
-
const typePoint = typeTuple([typeNumber(), typeNumber()]);
|
|
60
|
+
const typePoint = typeTuple([typeNumber("a"), typeNumber("b")]);
|
|
62
61
|
typePoint.decoder("3.14,2.71"); // → [3.14, 2.71]
|
|
63
|
-
typePoint.decoder("x,2"); // → Error: at 0:
|
|
62
|
+
typePoint.decoder("x,2"); // → Error: at 0: a: Unable to parse: "x"
|
|
64
63
|
```
|
|
65
64
|
|
|
66
65
|
The default separator is `","`. Pass a second argument to change it:
|
|
67
66
|
|
|
68
67
|
```ts
|
|
69
|
-
typeTuple([
|
|
68
|
+
typeTuple([typeString("name"), typeNumber()], ":");
|
|
70
69
|
// "foo:42" → ["foo", 42]
|
|
71
70
|
```
|
|
72
71
|
|
|
@@ -75,9 +74,9 @@ typeTuple([type("name"), typeNumber()], ":");
|
|
|
75
74
|
Splits a string into an array of typed values:
|
|
76
75
|
|
|
77
76
|
```ts
|
|
78
|
-
const typeNumbers = typeList(typeNumber());
|
|
77
|
+
const typeNumbers = typeList(typeNumber("v"));
|
|
79
78
|
typeNumbers.decoder("1,2,3"); // → [1, 2, 3]
|
|
80
|
-
typeNumbers.decoder("1,x,3"); // → Error: at 1:
|
|
79
|
+
typeNumbers.decoder("1,x,3"); // → Error: at 1: v: Unable to parse: "x"
|
|
81
80
|
```
|
|
82
81
|
|
|
83
82
|
Custom separator:
|
|
@@ -94,17 +93,19 @@ over `typeList` when users should pass multiple values as separate flags
|
|
|
94
93
|
|
|
95
94
|
:::
|
|
96
95
|
|
|
97
|
-
## `
|
|
96
|
+
## `typeMapped` — transformed decoded value
|
|
98
97
|
|
|
99
98
|
Chains a base type with a transformation function:
|
|
100
99
|
|
|
101
100
|
```ts
|
|
102
|
-
const typePort =
|
|
103
|
-
if (n < 1 || n > 65535)
|
|
101
|
+
const typePort = typeMapped("port", typeNumber(), (n) => {
|
|
102
|
+
if (n < 1 || n > 65535) {
|
|
103
|
+
throw new Error("Out of range");
|
|
104
|
+
}
|
|
104
105
|
return n;
|
|
105
106
|
});
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
typePort.decoder("8080"); // → 8080
|
|
108
|
+
typePort.decoder("99999"); // → Error: Out of range
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
## `typeRenamed` — rename a type
|
|
@@ -129,6 +130,6 @@ const typeHexColor: Type<string> = {
|
|
|
129
130
|
throw new Error(`Not a valid hex color: "${value}"`);
|
|
130
131
|
},
|
|
131
132
|
};
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
typeHexColor.decoder("#ff0000"); // → "#ff0000"
|
|
134
|
+
typeHexColor.decoder("red"); // → Error: Not a valid hex color: "red"
|
|
134
135
|
```
|
|
@@ -67,7 +67,7 @@ const rootCmd = commandWithSubcommands(
|
|
|
67
67
|
long: "db",
|
|
68
68
|
type: typeUrl(),
|
|
69
69
|
description: "Database URL",
|
|
70
|
-
|
|
70
|
+
fallbackValueIfAbsent: () => new URL("postgres://localhost/mydb"),
|
|
71
71
|
}),
|
|
72
72
|
},
|
|
73
73
|
positionals: [],
|
package/docs/index.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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:
|
|
@@ -10,7 +10,7 @@ hero:
|
|
|
10
10
|
Simple and Stupid, it just does the job.
|
|
11
11
|
|
|
12
12
|
image:
|
|
13
|
-
src: /
|
|
13
|
+
src: /logo.png
|
|
14
14
|
|
|
15
15
|
actions:
|
|
16
16
|
- theme: brand
|
package/docs/public/favicon.ico
CHANGED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export * from "./lib/Option";
|
|
|
4
4
|
export * from "./lib/Positional";
|
|
5
5
|
export * from "./lib/Reader";
|
|
6
6
|
export * from "./lib/Run";
|
|
7
|
-
export * from "./lib/
|
|
7
|
+
export * from "./lib/Suggest";
|
|
8
8
|
export * from "./lib/Type";
|
|
9
9
|
export * from "./lib/Typo";
|
|
10
10
|
export * from "./lib/Usage";
|
package/src/lib/Command.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Operation } from "./Operation";
|
|
2
2
|
import { ReaderArgs } from "./Reader";
|
|
3
|
-
import {
|
|
3
|
+
import { suggestTextPushMessage } from "./Suggest";
|
|
4
4
|
import {
|
|
5
5
|
TypoError,
|
|
6
6
|
TypoString,
|
|
@@ -17,7 +17,7 @@ import { UsageCommand } from "./Usage";
|
|
|
17
17
|
* @typeParam Context - Injected at execution; forwarded to handlers.
|
|
18
18
|
* @typeParam Result - Produced on execution; typically `void`.
|
|
19
19
|
*/
|
|
20
|
-
export type Command<Context, Result> = {
|
|
20
|
+
export type Command<Context, Result = void> = {
|
|
21
21
|
/**
|
|
22
22
|
* Returns static metadata.
|
|
23
23
|
*/
|
|
@@ -44,7 +44,7 @@ export type CommandDecoder<Context, Result> = {
|
|
|
44
44
|
/**
|
|
45
45
|
* Creates a ready-to-execute {@link CommandInterpreter}.
|
|
46
46
|
*
|
|
47
|
-
* @throws
|
|
47
|
+
* @throws if parsing or decoding failed.
|
|
48
48
|
*/
|
|
49
49
|
decodeAndMakeInterpreter(): CommandInterpreter<Context, Result>;
|
|
50
50
|
};
|
|
@@ -82,6 +82,7 @@ export type CommandInformation = {
|
|
|
82
82
|
* Shown in the `Examples:` section.
|
|
83
83
|
*/
|
|
84
84
|
examples?: Array<{
|
|
85
|
+
// TODO - a nicer example system, maybe with --help=example support
|
|
85
86
|
/**
|
|
86
87
|
* Explanation shown above the example.
|
|
87
88
|
*/
|
|
@@ -117,13 +118,13 @@ export type CommandInformation = {
|
|
|
117
118
|
* const greet = command(
|
|
118
119
|
* { description: "Greet a user" },
|
|
119
120
|
* operation(
|
|
120
|
-
* {
|
|
121
|
+
* { positionals: [positionalRequired({ type: type("name") })] },
|
|
121
122
|
* async (_ctx, { positionals: [name] }) => console.log(`Hello, ${name}!`),
|
|
122
123
|
* ),
|
|
123
124
|
* );
|
|
124
125
|
* ```
|
|
125
126
|
*/
|
|
126
|
-
export function command<Context, Result>(
|
|
127
|
+
export function command<Context, Result = void>(
|
|
127
128
|
information: CommandInformation,
|
|
128
129
|
operation: Operation<Context, Result>,
|
|
129
130
|
): Command<Context, Result> {
|
|
@@ -136,12 +137,11 @@ export function command<Context, Result>(
|
|
|
136
137
|
const operationDecoder = operation.consumeAndMakeDecoder(readerArgs);
|
|
137
138
|
const endPositional = readerArgs.consumePositional();
|
|
138
139
|
if (endPositional !== undefined) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
140
|
+
const errorText = new TypoText();
|
|
141
|
+
errorText.push(new TypoString(`Unexpected argument: `));
|
|
142
|
+
errorText.push(new TypoString(`"${endPositional}"`, typoStyleQuote));
|
|
143
|
+
errorText.push(new TypoString(`.`));
|
|
144
|
+
throw new TypoError(errorText);
|
|
145
145
|
}
|
|
146
146
|
return {
|
|
147
147
|
generateUsage: () => generateUsageLeaf(information, operation),
|
|
@@ -168,7 +168,8 @@ export function command<Context, Result>(
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
|
-
* Creates a command that runs `operation` first,
|
|
171
|
+
* Creates a command that runs `operation` first,
|
|
172
|
+
* then dispatches result to a named subcommand.
|
|
172
173
|
*
|
|
173
174
|
* @typeParam Context - Context accepted by `operation`.
|
|
174
175
|
* @typeParam Payload - Output of `operation`; becomes the subcommand's context.
|
|
@@ -191,7 +192,7 @@ export function command<Context, Result>(
|
|
|
191
192
|
* );
|
|
192
193
|
* ```
|
|
193
194
|
*/
|
|
194
|
-
export function commandWithSubcommands<Context, Payload, Result>(
|
|
195
|
+
export function commandWithSubcommands<Context, Payload, Result = void>(
|
|
195
196
|
information: CommandInformation,
|
|
196
197
|
operation: Operation<Context, Payload>,
|
|
197
198
|
subcommands: { [subcommand: string]: Command<Payload, Result> },
|
|
@@ -200,6 +201,11 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
200
201
|
if (subcommandNames.length === 0) {
|
|
201
202
|
throw new Error("At least one subcommand is required");
|
|
202
203
|
}
|
|
204
|
+
for (const name of subcommandNames) {
|
|
205
|
+
if (name.startsWith("-")) {
|
|
206
|
+
throw new Error(`Subcommand name "${name}" cannot start with "-".`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
203
209
|
return {
|
|
204
210
|
getInformation() {
|
|
205
211
|
return information;
|
|
@@ -209,30 +215,22 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
209
215
|
const operationDecoder = operation.consumeAndMakeDecoder(readerArgs);
|
|
210
216
|
const subcommandName = readerArgs.consumePositional();
|
|
211
217
|
if (subcommandName === undefined) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
218
|
+
const errorText = new TypoText();
|
|
219
|
+
errorText.push(new TypoString(`Missing argument: `));
|
|
220
|
+
errorText.push(new TypoString(`<subcommand>`, typoStyleUserInput));
|
|
221
|
+
errorText.push(new TypoString(`.`));
|
|
222
|
+
suggestSubcommandNames(errorText, "", subcommandNames);
|
|
223
|
+
throw new TypoError(errorText);
|
|
218
224
|
}
|
|
219
225
|
const subcommandInput = subcommands[subcommandName];
|
|
220
226
|
if (subcommandInput === undefined) {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
key: subcommandName,
|
|
229
|
-
value: new TypoString(subcommandName, typoStyleConstants),
|
|
230
|
-
})),
|
|
231
|
-
).slice(0, 3);
|
|
232
|
-
text.push(new TypoString(`: did you mean: `));
|
|
233
|
-
text.push(TypoText.join(suggestions, new TypoString(`, `)));
|
|
234
|
-
text.push(new TypoString(` ?`));
|
|
235
|
-
throw new TypoError(text);
|
|
227
|
+
const errorText = new TypoText();
|
|
228
|
+
errorText.push(new TypoString(`<subcommand>`, typoStyleUserInput));
|
|
229
|
+
errorText.push(new TypoString(`: Unknown name: `));
|
|
230
|
+
errorText.push(new TypoString(`"${subcommandName}"`, typoStyleQuote));
|
|
231
|
+
errorText.push(new TypoString(`.`));
|
|
232
|
+
suggestSubcommandNames(errorText, subcommandName, subcommandNames);
|
|
233
|
+
throw new TypoError(errorText);
|
|
236
234
|
}
|
|
237
235
|
const subcommandDecoder =
|
|
238
236
|
subcommandInput.consumeAndMakeDecoder(readerArgs);
|
|
@@ -283,8 +281,8 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
283
281
|
}
|
|
284
282
|
|
|
285
283
|
/**
|
|
286
|
-
* Chains an {@link Operation} and a {@link Command}: `operation` runs first,
|
|
287
|
-
* output becomes `subcommand`'s context. No token is consumed for routing.
|
|
284
|
+
* Chains an {@link Operation} and a {@link Command}: `operation` runs first,
|
|
285
|
+
* its output becomes `subcommand`'s context. No token is consumed for routing.
|
|
288
286
|
*
|
|
289
287
|
* @typeParam Context - Context accepted by `operation`.
|
|
290
288
|
* @typeParam Payload - Output of `operation`; becomes `subcommand`'s context.
|
|
@@ -295,7 +293,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
295
293
|
* @param subcommand - Runs after `operation`.
|
|
296
294
|
* @returns A {@link Command} composing both stages.
|
|
297
295
|
*/
|
|
298
|
-
export function commandChained<Context, Payload, Result>(
|
|
296
|
+
export function commandChained<Context, Payload, Result = void>(
|
|
299
297
|
information: CommandInformation,
|
|
300
298
|
operation: Operation<Context, Payload>,
|
|
301
299
|
subcommand: Command<Payload, Result>,
|
|
@@ -355,12 +353,25 @@ function generateUsageLeaf(
|
|
|
355
353
|
): UsageCommand {
|
|
356
354
|
const { positionals, options } = operation.generateUsage();
|
|
357
355
|
return {
|
|
358
|
-
segments: positionals.map((
|
|
359
|
-
positional: positional.label,
|
|
360
|
-
})),
|
|
356
|
+
segments: positionals.map((p) => ({ positional: p.label })),
|
|
361
357
|
information,
|
|
362
358
|
positionals,
|
|
363
359
|
subcommands: [],
|
|
364
360
|
options,
|
|
365
361
|
};
|
|
366
362
|
}
|
|
363
|
+
|
|
364
|
+
function suggestSubcommandNames(
|
|
365
|
+
errorText: TypoText,
|
|
366
|
+
input: string,
|
|
367
|
+
subcommandNames: Array<string> = [],
|
|
368
|
+
) {
|
|
369
|
+
suggestTextPushMessage(
|
|
370
|
+
errorText,
|
|
371
|
+
input,
|
|
372
|
+
subcommandNames.map((subcommandName) => ({
|
|
373
|
+
reference: subcommandName,
|
|
374
|
+
hint: new TypoString(subcommandName, typoStyleConstants),
|
|
375
|
+
})),
|
|
376
|
+
);
|
|
377
|
+
}
|
package/src/lib/Operation.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { UsageOption, UsagePositional } from "./Usage";
|
|
|
12
12
|
* @typeParam Context - Injected at execution; forwarded to handlers.
|
|
13
13
|
* @typeParam Result - Value produced on execution; typically `void`.
|
|
14
14
|
*/
|
|
15
|
-
export type Operation<Context, Result> = {
|
|
15
|
+
export type Operation<Context, Result = void> = {
|
|
16
16
|
/**
|
|
17
17
|
* Returns usage metadata without consuming any arguments.
|
|
18
18
|
*/
|
|
@@ -44,7 +44,7 @@ export type OperationDecoder<Context, Result> = {
|
|
|
44
44
|
/**
|
|
45
45
|
* Creates a ready-to-execute {@link OperationInterpreter}.
|
|
46
46
|
*
|
|
47
|
-
* @throws
|
|
47
|
+
* @throws if parsing or decoding failed.
|
|
48
48
|
*/
|
|
49
49
|
decodeAndMakeInterpreter(): OperationInterpreter<Context, Result>;
|
|
50
50
|
};
|
|
@@ -99,55 +99,71 @@ export type OperationInterpreter<Context, Result> = {
|
|
|
99
99
|
export function operation<
|
|
100
100
|
Context,
|
|
101
101
|
Result,
|
|
102
|
-
Options extends { [option: string]: any },
|
|
103
|
-
const Positionals extends Array<any
|
|
102
|
+
const Options extends { [option: string]: any } = {},
|
|
103
|
+
const Positionals extends Array<any> = [],
|
|
104
104
|
>(
|
|
105
105
|
inputs: {
|
|
106
|
-
options
|
|
107
|
-
positionals
|
|
106
|
+
options?: { [K in keyof Options]: Option<Options[K]> };
|
|
107
|
+
positionals?: { [K in keyof Positionals]: Positional<Positionals[K]> };
|
|
108
108
|
},
|
|
109
109
|
handler: (
|
|
110
110
|
context: Context,
|
|
111
111
|
inputs: {
|
|
112
|
-
options: Options;
|
|
113
|
-
positionals: Positionals;
|
|
112
|
+
options: { [K in keyof Options]: Options[K] };
|
|
113
|
+
positionals: { [K in keyof Positionals]: Positionals[K] };
|
|
114
114
|
},
|
|
115
115
|
) => Promise<Result>,
|
|
116
116
|
): Operation<Context, Result> {
|
|
117
117
|
return {
|
|
118
118
|
generateUsage() {
|
|
119
119
|
const optionsUsage = new Array<UsageOption>();
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
120
|
+
if (inputs.options !== undefined) {
|
|
121
|
+
for (const optionKey in inputs.options) {
|
|
122
|
+
const optionInput = inputs.options[optionKey]!;
|
|
123
|
+
optionsUsage.push(optionInput.generateUsage());
|
|
124
|
+
}
|
|
123
125
|
}
|
|
124
126
|
const positionalsUsage = new Array<UsagePositional>();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
if (inputs.positionals !== undefined) {
|
|
128
|
+
for (const positionalInput of inputs.positionals) {
|
|
129
|
+
positionalsUsage.push(positionalInput.generateUsage());
|
|
130
|
+
}
|
|
127
131
|
}
|
|
128
132
|
return { options: optionsUsage, positionals: positionalsUsage };
|
|
129
133
|
},
|
|
130
134
|
consumeAndMakeDecoder(readerArgs: ReaderArgs) {
|
|
131
|
-
const optionsDecoders
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
const optionsDecoders = {} as {
|
|
136
|
+
[K in keyof Options]: OptionDecoder<Options[K]>;
|
|
137
|
+
};
|
|
138
|
+
if (inputs.options !== undefined) {
|
|
139
|
+
for (const optionKey in inputs.options) {
|
|
140
|
+
const optionInput = inputs.options[optionKey]!;
|
|
141
|
+
optionsDecoders[optionKey] =
|
|
142
|
+
optionInput.registerAndMakeDecoder(readerArgs);
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
|
-
const positionalsDecoders
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
145
|
+
const positionalsDecoders = [] as {
|
|
146
|
+
[K in keyof Positionals]: PositionalDecoder<Positionals[K]>;
|
|
147
|
+
};
|
|
148
|
+
if (inputs.positionals !== undefined) {
|
|
149
|
+
for (const positionalInput of inputs.positionals) {
|
|
150
|
+
positionalsDecoders.push(
|
|
151
|
+
positionalInput.consumeAndMakeDecoder(readerArgs),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
142
154
|
}
|
|
143
155
|
return {
|
|
144
156
|
decodeAndMakeInterpreter() {
|
|
145
|
-
const optionsValues
|
|
157
|
+
const optionsValues = {} as {
|
|
158
|
+
[K in keyof Options]: Options[K];
|
|
159
|
+
};
|
|
146
160
|
for (const optionKey in optionsDecoders) {
|
|
147
161
|
optionsValues[optionKey] =
|
|
148
162
|
optionsDecoders[optionKey]!.getAndDecodeValue();
|
|
149
163
|
}
|
|
150
|
-
const positionalsValues
|
|
164
|
+
const positionalsValues = [] as {
|
|
165
|
+
[K in keyof Positionals]: Positionals[K];
|
|
166
|
+
};
|
|
151
167
|
for (const positionalDecoder of positionalsDecoders) {
|
|
152
168
|
positionalsValues.push(positionalDecoder.decodeValue());
|
|
153
169
|
}
|