cli-kiss 0.0.3 → 0.0.4

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.
@@ -4,7 +4,7 @@ import { OptionUsage } from "./Option";
4
4
  import { ReaderTokenizer } from "./Reader";
5
5
 
6
6
  export type Command<Context, Result> = {
7
- getTitle(): string | undefined;
7
+ getDescription(): string | undefined;
8
8
  prepareRunner(
9
9
  readerTokenizer: ReaderTokenizer,
10
10
  ): CommandRunner<Context, Result>;
@@ -17,28 +17,26 @@ export type CommandRunner<Context, Result> = {
17
17
 
18
18
  export type CommandUsage = {
19
19
  breadcrumbs: Array<CommandUsageBreadcrumb>;
20
- title: string;
21
- description: Array<string> | undefined;
20
+ description: string;
21
+ details: Array<string> | undefined;
22
22
  options: Array<OptionUsage>;
23
23
  arguments: Array<ArgumentUsage>;
24
- subcommands: Array<{ name: string; title: string | undefined }>;
24
+ subcommands: Array<{ name: string; description: string | undefined }>;
25
25
  };
26
26
 
27
- export type CommandUsageBreadcrumb = {
28
- kind: "command" | "argument";
29
- value: string;
30
- };
27
+ export type CommandUsageBreadcrumb = { argument: string } | { command: string };
31
28
 
32
29
  export function command<Context, Result>(
33
30
  metadata: {
34
- title: string;
35
- description?: Array<string>;
31
+ description: string;
32
+ details?: Array<string>;
33
+ // TODO - examples ?
36
34
  },
37
35
  execution: Execution<Context, Result>,
38
36
  ): Command<Context, Result> {
39
37
  return {
40
- getTitle() {
41
- return metadata.title;
38
+ getDescription() {
39
+ return metadata.description;
42
40
  },
43
41
  prepareRunner(readerTokenizer: ReaderTokenizer) {
44
42
  function computeUsage(): CommandUsage {
@@ -47,8 +45,8 @@ export function command<Context, Result>(
47
45
  breadcrumbs: executionUsage.arguments.map((argument) =>
48
46
  breadcrumbArgument(argument.label),
49
47
  ),
50
- title: metadata.title,
51
48
  description: metadata.description,
49
+ details: metadata.details,
52
50
  options: executionUsage.options,
53
51
  arguments: executionUsage.arguments,
54
52
  subcommands: [],
@@ -81,15 +79,15 @@ export function command<Context, Result>(
81
79
 
82
80
  export function commandWithSubcommands<Context, Payload, Result>(
83
81
  metadata: {
84
- title: string;
85
- description?: Array<string>;
82
+ description: string;
83
+ details?: Array<string>;
86
84
  },
87
85
  execution: Execution<Context, Payload>,
88
86
  subcommands: { [subcommand: Lowercase<string>]: Command<Payload, Result> },
89
87
  ): Command<Context, Result> {
90
88
  return {
91
- getTitle() {
92
- return metadata.title;
89
+ getDescription() {
90
+ return metadata.description;
93
91
  },
94
92
  prepareRunner(readerTokenizer: ReaderTokenizer) {
95
93
  try {
@@ -114,8 +112,8 @@ export function commandWithSubcommands<Context, Payload, Result>(
114
112
  .map((argument) => breadcrumbArgument(argument.label))
115
113
  .concat([breadcrumbCommand(subcommandName)])
116
114
  .concat(subcommandUsage.breadcrumbs),
117
- title: subcommandUsage.title,
118
115
  description: subcommandUsage.description,
116
+ details: subcommandUsage.details,
119
117
  options: executionUsage.options.concat(subcommandUsage.options),
120
118
  arguments: executionUsage.arguments.concat(
121
119
  subcommandUsage.arguments,
@@ -136,14 +134,14 @@ export function commandWithSubcommands<Context, Payload, Result>(
136
134
  breadcrumbs: executionUsage.arguments
137
135
  .map((argument) => breadcrumbArgument(argument.label))
138
136
  .concat([breadcrumbCommand("<SUBCOMMAND>")]),
139
- title: metadata.title,
140
137
  description: metadata.description,
138
+ details: metadata.details,
141
139
  options: executionUsage.options,
142
140
  arguments: executionUsage.arguments,
143
141
  subcommands: Object.entries(subcommands).map(
144
142
  ([name, subcommand]) => ({
145
143
  name,
146
- title: subcommand.getTitle(),
144
+ description: subcommand.getDescription(),
147
145
  }),
148
146
  ),
149
147
  };
@@ -157,10 +155,10 @@ export function commandWithSubcommands<Context, Payload, Result>(
157
155
  };
158
156
  }
159
157
 
160
- function breadcrumbArgument(label: string): CommandUsageBreadcrumb {
161
- return { kind: "argument", value: label };
158
+ function breadcrumbArgument(value: string): CommandUsageBreadcrumb {
159
+ return { argument: value };
162
160
  }
163
161
 
164
- function breadcrumbCommand(name: string): CommandUsageBreadcrumb {
165
- return { kind: "command", value: name };
162
+ function breadcrumbCommand(value: string): CommandUsageBreadcrumb {
163
+ return { command: value };
166
164
  }
@@ -26,21 +26,18 @@ export type ExecutionUsage = {
26
26
  export function execution<
27
27
  Context,
28
28
  Result,
29
- Options extends { [option: string]: Option<any> },
30
- const Arguments extends Array<Argument<any>>,
29
+ Options extends { [option: string]: any },
30
+ const Arguments extends Array<any>,
31
31
  >(
32
- inputs: { options: Options; arguments: Arguments },
32
+ inputs: {
33
+ options: { [K in keyof Options]: Option<Options[K]> };
34
+ arguments: { [K in keyof Arguments]: Argument<Arguments[K]> };
35
+ },
33
36
  handler: (
34
37
  context: Context,
35
38
  inputs: {
36
- options: {
37
- [K in keyof Options]: ReturnType<
38
- ReturnType<Options[K]["prepareConsumer"]>
39
- >;
40
- };
41
- arguments: {
42
- [K in keyof Arguments]: ReturnType<Arguments[K]["consumeValue"]>;
43
- };
39
+ options: Options;
40
+ arguments: Arguments;
44
41
  },
45
42
  ) => Promise<Result>,
46
43
  ): Execution<Context, Result> {
package/src/lib/Grid.ts CHANGED
@@ -4,7 +4,11 @@ export type Grid = Array<GridRow>;
4
4
  export type GridRow = Array<GridCell>;
5
5
  export type GridCell = Array<TypoText>;
6
6
 
7
- export function gridToPrintableLines(grid: Grid, typoSupport: TypoSupport) {
7
+ export function gridToPrintableLines(
8
+ grid: Grid,
9
+ typoSupport: TypoSupport,
10
+ delimiter: string = "",
11
+ ): Array<string> {
8
12
  const lines = new Array<string>();
9
13
  const gridWidths = new Array<number>();
10
14
  for (const gridRow of grid) {
@@ -42,7 +46,7 @@ export function gridToPrintableLines(grid: Grid, typoSupport: TypoSupport) {
42
46
  lineColumns.push(parts.join(""));
43
47
  }
44
48
  }
45
- lines.push(lineColumns.join(" "));
49
+ lines.push(lineColumns.join(delimiter));
46
50
  }
47
51
  return lines;
48
52
  }
package/src/lib/Run.ts CHANGED
@@ -3,18 +3,17 @@ import { ReaderTokenizer } from "./Reader";
3
3
  import { typoInferSupport } from "./Typo";
4
4
  import { usageToPrintableLines } from "./Usage";
5
5
 
6
- export async function runWithArgv<Context, Result>(
7
- argv: string[],
6
+ export async function runCommand<Context, Result>(
7
+ cliName: Lowercase<string>,
8
+ cliArgs: Array<string>,
8
9
  context: Context,
9
10
  command: Command<Context, Result>,
10
11
  cliInfo?: {
11
- name?: Lowercase<string>;
12
12
  version?: string;
13
13
  helpOnError?: boolean;
14
14
  },
15
15
  ): Promise<Result> {
16
- const cliName = cliInfo?.name ?? argv[1]!;
17
- const readerTokenizer = new ReaderTokenizer(argv.slice(2));
16
+ const readerTokenizer = new ReaderTokenizer(cliArgs);
18
17
  if (cliInfo?.version) {
19
18
  readerTokenizer.registerFlag({
20
19
  key: "version",
@@ -65,7 +64,7 @@ export async function runWithArgv<Context, Result>(
65
64
  }).join("\n"),
66
65
  );
67
66
  }
68
- console.error(error);
67
+ console.error(error); // TODO - better, prettier errors
69
68
  process.exit(1);
70
69
  }
71
70
  } catch (error) {
package/src/lib/Type.ts CHANGED
@@ -48,10 +48,35 @@ export const typeBigInt: Type<bigint> = {
48
48
  },
49
49
  };
50
50
 
51
- export function typeCommaArray(elementType: Type<any>): Type<Array<any>> {
51
+ export function typeCommaTuple<
52
+ const Elements extends Array<any>,
53
+ >(elementTypes: {
54
+ [K in keyof Elements]: Type<Elements[K]>;
55
+ }): Type<Elements> {
56
+ return {
57
+ label: elementTypes
58
+ .map((elementType) => elementType.label)
59
+ .join(",") as Uppercase<string>,
60
+ decoder(value: string) {
61
+ const parts = value.split(",", elementTypes.length);
62
+ if (parts.length !== elementTypes.length) {
63
+ throw new Error(
64
+ `Invalid tuple value: ${value}, expected ${elementTypes.length} parts`,
65
+ );
66
+ }
67
+ return parts.map((part, index) =>
68
+ elementTypes[index]!.decoder(part),
69
+ ) as Elements;
70
+ },
71
+ };
72
+ }
73
+
74
+ export function typeCommaList<Value>(
75
+ elementType: Type<Value>,
76
+ ): Type<Array<Value>> {
52
77
  return {
53
78
  label:
54
- `${elementType.label}[${elementType.label},...]` as Uppercase<string>,
79
+ `${elementType.label}[,${elementType.label}...]` as Uppercase<string>,
55
80
  decoder(value: string) {
56
81
  return value.split(",").map(elementType.decoder);
57
82
  },
package/src/lib/Typo.ts CHANGED
@@ -63,11 +63,20 @@ export function typoInferSupport(): TypoSupport {
63
63
  const resetCode = "\x1b[0m";
64
64
  const boldCode = "\x1b[1m";
65
65
  const colorCodes = {
66
- red: "\x1b[31m",
67
- green: "\x1b[32m",
68
- yellow: "\x1b[33m",
69
- blue: "\x1b[34m",
70
- magenta: "\x1b[35m",
71
- cyan: "\x1b[36m",
72
- grey: "\x1b[90m",
66
+ darkBlack: "\x1b[30m",
67
+ darkRed: "\x1b[31m",
68
+ darkGreen: "\x1b[32m",
69
+ darkYellow: "\x1b[33m",
70
+ darkBlue: "\x1b[34m",
71
+ darkMagenta: "\x1b[35m",
72
+ darkCyan: "\x1b[36m",
73
+ darkWhite: "\x1b[37m",
74
+ brightBlack: "\x1b[90m",
75
+ brightRed: "\x1b[91m",
76
+ brightGreen: "\x1b[92m",
77
+ brightYellow: "\x1b[93m",
78
+ brightBlue: "\x1b[94m",
79
+ brightMagenta: "\x1b[95m",
80
+ brightCyan: "\x1b[96m",
81
+ brightWhite: "\x1b[97m",
73
82
  };
package/src/lib/Usage.ts CHANGED
@@ -3,7 +3,7 @@ import { GridCell, GridRow, gridToPrintableLines } from "./Grid";
3
3
  import { TypoSupport, TypoText, typoPrintableString } from "./Typo";
4
4
 
5
5
  export function usageToPrintableLines(params: {
6
- cliName: string;
6
+ cliName: Lowercase<string>;
7
7
  commandUsage: CommandUsage;
8
8
  typoSupport: TypoSupport;
9
9
  }) {
@@ -11,42 +11,61 @@ export function usageToPrintableLines(params: {
11
11
 
12
12
  const lines = new Array<string>();
13
13
 
14
- lines.push("");
15
- lines.push(typoPrintableString(typoSupport, textDesc(commandUsage.title)));
16
- if (commandUsage.description) {
17
- for (const descriptionLine of commandUsage.description) {
18
- lines.push(typoPrintableString(typoSupport, textSubs(descriptionLine)));
14
+ lines.push(
15
+ typoPrintableString(typoSupport, textDescription(commandUsage.description)),
16
+ );
17
+ if (commandUsage.details) {
18
+ for (const detailLine of commandUsage.details) {
19
+ lines.push(typoPrintableString(typoSupport, textDetails(detailLine)));
19
20
  }
20
21
  }
21
22
 
22
- const breadcrumbPrefix = typoPrintableString(
23
- typoSupport,
24
- textTitle("Usage:"),
25
- );
26
- const breadcrumbsItems = [
27
- typoPrintableString(typoSupport, textName(cliName)),
23
+ lines.push("");
24
+ lines.push(typoPrintableString(typoSupport, textCategory("Usage:")));
25
+ const breadcrumbs = [
26
+ " ",
27
+ typoPrintableString(typoSupport, textFixed(cliName)),
28
28
  ].concat(
29
29
  commandUsage.breadcrumbs.map((breadcrumb) => {
30
- if (breadcrumb.kind === "argument") {
31
- return typoPrintableString(typoSupport, textValue(breadcrumb.value));
30
+ if ("argument" in breadcrumb) {
31
+ return typoPrintableString(typoSupport, textInput(breadcrumb.argument));
32
+ }
33
+ if ("command" in breadcrumb) {
34
+ return typoPrintableString(typoSupport, textFixed(breadcrumb.command));
32
35
  }
33
- return typoPrintableString(typoSupport, textName(breadcrumb.value));
36
+ throw new Error(`Unknown breadcrumb: ${JSON.stringify(breadcrumb)}`);
34
37
  }),
35
38
  );
36
- lines.push("");
37
- lines.push(`${breadcrumbPrefix} ${breadcrumbsItems.join(" ")}`);
39
+ lines.push(breadcrumbs.join(" "));
38
40
 
39
41
  if (commandUsage.arguments.length > 0) {
40
42
  lines.push("");
41
- lines.push(typoPrintableString(typoSupport, textTitle("Arguments:")));
43
+ lines.push(typoPrintableString(typoSupport, textCategory("Arguments:")));
42
44
  const grid = new Array<GridRow>();
43
45
  for (const argumentUsage of commandUsage.arguments) {
44
46
  const gridRow = new Array<GridCell>();
45
- gridRow.push([]);
46
- gridRow.push([textValue(argumentUsage.label)]);
47
- gridRow.push([]);
47
+ gridRow.push([textDelimiter()]);
48
+ gridRow.push([textInput(argumentUsage.label)]);
48
49
  if (argumentUsage.description) {
49
- gridRow.push([textDesc(argumentUsage.description)]);
50
+ gridRow.push([textDelimiter()]);
51
+ gridRow.push([textDescription(argumentUsage.description)]);
52
+ }
53
+ grid.push(gridRow);
54
+ }
55
+ lines.push(...gridToPrintableLines(grid, typoSupport));
56
+ }
57
+
58
+ if (commandUsage.subcommands.length > 0) {
59
+ lines.push("");
60
+ lines.push(typoPrintableString(typoSupport, textCategory("Subcommands:")));
61
+ const grid = new Array<GridRow>();
62
+ for (const subcommand of commandUsage.subcommands) {
63
+ const gridRow = new Array<GridCell>();
64
+ gridRow.push([textDelimiter()]);
65
+ gridRow.push([textFixed(subcommand.name)]);
66
+ if (subcommand.description) {
67
+ gridRow.push([textDelimiter()]);
68
+ gridRow.push([textDescription(subcommand.description)]);
50
69
  }
51
70
  grid.push(gridRow);
52
71
  }
@@ -55,72 +74,57 @@ export function usageToPrintableLines(params: {
55
74
 
56
75
  if (commandUsage.options.length > 0) {
57
76
  lines.push("");
58
- lines.push(typoPrintableString(typoSupport, textTitle("Options:")));
77
+ lines.push(typoPrintableString(typoSupport, textCategory("Options:")));
59
78
  const grid = new Array<GridRow>();
60
79
  for (const optionUsage of commandUsage.options) {
61
80
  const gridRow = new Array<GridCell>();
62
- gridRow.push([]);
81
+ gridRow.push([textDelimiter()]);
63
82
  if (optionUsage.short) {
64
- gridRow.push([textName(`-${optionUsage.short}`), { value: "," }]);
83
+ gridRow.push([textFixed(`-${optionUsage.short}`), { value: ", " }]);
65
84
  } else {
66
85
  gridRow.push([]);
67
86
  }
68
87
  if (optionUsage.label) {
69
88
  gridRow.push([
70
- textName(`--${optionUsage.long} `),
71
- textValue(optionUsage.label),
89
+ textFixed(`--${optionUsage.long} `),
90
+ textInput(optionUsage.label),
72
91
  ]);
73
92
  } else {
74
- gridRow.push([
75
- textName(`--${optionUsage.long}`),
76
- { value: "[=yes|no]", color: "grey" },
77
- ]);
93
+ gridRow.push([textFixed(`--${optionUsage.long}`)]);
78
94
  }
79
- gridRow.push([]);
80
95
  if (optionUsage.description) {
81
- gridRow.push([textDesc(optionUsage.description)]);
96
+ gridRow.push([textDelimiter()]);
97
+ gridRow.push([textDescription(optionUsage.description)]);
82
98
  }
83
99
  grid.push(gridRow);
84
100
  }
85
101
  lines.push(...gridToPrintableLines(grid, typoSupport));
86
102
  }
87
103
 
88
- if (commandUsage.subcommands.length > 0) {
89
- lines.push("");
90
- lines.push(typoPrintableString(typoSupport, textTitle("Subcommands:")));
91
- const grid = new Array<GridRow>();
92
- for (const subcommand of commandUsage.subcommands) {
93
- const gridRow = new Array<GridCell>();
94
- gridRow.push([]);
95
- gridRow.push([textName(subcommand.name)]);
96
- gridRow.push([]);
97
- if (subcommand.title) {
98
- gridRow.push([textDesc(subcommand.title)]);
99
- }
100
- grid.push(gridRow);
101
- }
102
- lines.push(...gridToPrintableLines(grid, typoSupport));
103
- }
104
104
  lines.push("");
105
105
  return lines;
106
106
  }
107
107
 
108
- function textTitle(text: string): TypoText {
109
- return { value: text, color: "green", bold: true };
108
+ function textCategory(text: string): TypoText {
109
+ return { value: text, color: "brightGreen", bold: true };
110
110
  }
111
111
 
112
- function textSubs(text: string): TypoText {
113
- return { value: text, color: "grey" };
112
+ function textDescription(text: string): TypoText {
113
+ return { value: text, bold: true };
114
114
  }
115
115
 
116
- function textDesc(text: string): TypoText {
117
- return { value: text, bold: true };
116
+ function textDetails(text: string): TypoText {
117
+ return { value: text, color: "brightBlack" };
118
+ }
119
+
120
+ function textFixed(text: string): TypoText {
121
+ return { value: text, color: "brightCyan", bold: true };
118
122
  }
119
123
 
120
- function textName(text: string): TypoText {
121
- return { value: text, color: "cyan", bold: true };
124
+ function textInput(text: string): TypoText {
125
+ return { value: text, color: "brightBlue" };
122
126
  }
123
127
 
124
- function textValue(text: string): TypoText {
125
- return { value: text, color: "cyan" };
128
+ function textDelimiter(): TypoText {
129
+ return { value: " " };
126
130
  }
@@ -9,13 +9,14 @@ import {
9
9
  optionFlag,
10
10
  optionRepeatable,
11
11
  optionSingleValue,
12
- runWithArgv,
12
+ runCommand,
13
+ typeCommaList,
13
14
  typeNumber,
14
15
  typeString,
15
16
  } from "../src";
16
17
 
17
18
  const cmd = commandWithSubcommands<string, any, any>(
18
- { title: "Root command title" },
19
+ { description: "Root command description" },
19
20
  execution(
20
21
  {
21
22
  options: {
@@ -27,7 +28,7 @@ const cmd = commandWithSubcommands<string, any, any>(
27
28
  }),
28
29
  numberOption: optionRepeatable({
29
30
  long: "number-option",
30
- type: typeNumber,
31
+ type: typeCommaList(typeNumber),
31
32
  }),
32
33
  },
33
34
  arguments: [
@@ -41,7 +42,7 @@ const cmd = commandWithSubcommands<string, any, any>(
41
42
  ),
42
43
  {
43
44
  sub1: command(
44
- { title: "Subcommand 1 title" },
45
+ { description: "Subcommand 1 description" },
45
46
  execution(
46
47
  {
47
48
  options: {},
@@ -53,7 +54,7 @@ const cmd = commandWithSubcommands<string, any, any>(
53
54
  ),
54
55
  ),
55
56
  sub2: command(
56
- { title: "Subcommand 2 title" },
57
+ { description: "Subcommand 2 description" },
57
58
  execution(
58
59
  {
59
60
  options: {},
@@ -72,8 +73,9 @@ const cmd = commandWithSubcommands<string, any, any>(
72
73
  );
73
74
 
74
75
  it("run", async () => {
75
- const res1 = await runWithArgv(
76
- ["node", "script", "50", "51", "sub1", "final"],
76
+ const res1 = await runCommand(
77
+ "script",
78
+ ["50", "51", "sub1", "final"],
77
79
  "Run Context Input",
78
80
  cmd,
79
81
  );
@@ -97,18 +99,17 @@ it("run", async () => {
97
99
  at: "sub1",
98
100
  });
99
101
 
100
- const res2 = await runWithArgv(
102
+ const res2 = await runCommand(
103
+ "script",
101
104
  [
102
- "node",
103
- "script",
104
105
  "40",
105
106
  "41",
106
107
  "sub2",
107
108
  "--string-option=hello",
108
109
  "--number-option",
109
- "123",
110
+ "123.1,123.2",
110
111
  "--number-option",
111
- "1234",
112
+ "123.3",
112
113
  "88.88",
113
114
  "a,b",
114
115
  "final",
@@ -124,7 +125,7 @@ it("run", async () => {
124
125
  options: {
125
126
  booleanFlag: true,
126
127
  stringOption: "hello",
127
- numberOption: [123, 1234],
128
+ numberOption: [[123.1, 123.2], [123.3]],
128
129
  },
129
130
  arguments: [40, 41],
130
131
  },