cli-kiss 0.0.3 → 0.0.5

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,58 +4,58 @@ import { OptionUsage } from "./Option";
4
4
  import { ReaderTokenizer } from "./Reader";
5
5
 
6
6
  export type Command<Context, Result> = {
7
- getTitle(): string | undefined;
8
- prepareRunner(
7
+ getDescription(): string | undefined;
8
+ buildInterpreter(
9
9
  readerTokenizer: ReaderTokenizer,
10
- ): CommandRunner<Context, Result>;
10
+ ): CommandInterpreter<Context, Result>;
11
11
  };
12
12
 
13
- export type CommandRunner<Context, Result> = {
13
+ export type CommandMetadata = {
14
+ description: string;
15
+ details?: Array<string>;
16
+ // TODO - printable examples ?
17
+ };
18
+
19
+ export type CommandInterpreter<Context, Result> = {
14
20
  computeUsage(): CommandUsage;
15
21
  execute(context: Context): Promise<Result>;
16
22
  };
17
23
 
18
24
  export type CommandUsage = {
19
25
  breadcrumbs: Array<CommandUsageBreadcrumb>;
20
- title: string;
21
- description: Array<string> | undefined;
26
+ description: string;
27
+ details: Array<string> | undefined;
22
28
  options: Array<OptionUsage>;
23
29
  arguments: Array<ArgumentUsage>;
24
- subcommands: Array<{ name: string; title: string | undefined }>;
30
+ subcommands: Array<{ name: string; description: string | undefined }>;
25
31
  };
26
32
 
27
- export type CommandUsageBreadcrumb = {
28
- kind: "command" | "argument";
29
- value: string;
30
- };
33
+ export type CommandUsageBreadcrumb = { argument: string } | { command: string };
31
34
 
32
35
  export function command<Context, Result>(
33
- metadata: {
34
- title: string;
35
- description?: Array<string>;
36
- },
36
+ metadata: CommandMetadata,
37
37
  execution: Execution<Context, Result>,
38
38
  ): Command<Context, Result> {
39
39
  return {
40
- getTitle() {
41
- return metadata.title;
40
+ getDescription() {
41
+ return metadata.description;
42
42
  },
43
- prepareRunner(readerTokenizer: ReaderTokenizer) {
43
+ buildInterpreter(readerTokenizer: ReaderTokenizer) {
44
44
  function computeUsage(): CommandUsage {
45
45
  const executionUsage = execution.computeUsage();
46
46
  return {
47
47
  breadcrumbs: executionUsage.arguments.map((argument) =>
48
48
  breadcrumbArgument(argument.label),
49
49
  ),
50
- title: metadata.title,
51
50
  description: metadata.description,
51
+ details: metadata.details,
52
52
  options: executionUsage.options,
53
53
  arguments: executionUsage.arguments,
54
54
  subcommands: [],
55
55
  };
56
56
  }
57
57
  try {
58
- const executionResolver = execution.prepareResolver(readerTokenizer);
58
+ const executionResolver = execution.createResolver(readerTokenizer);
59
59
  const lastPositional = readerTokenizer.consumePositional();
60
60
  if (lastPositional !== undefined) {
61
61
  throw Error(`Unprocessed positional: ${lastPositional}`);
@@ -80,20 +80,17 @@ export function command<Context, Result>(
80
80
  }
81
81
 
82
82
  export function commandWithSubcommands<Context, Payload, Result>(
83
- metadata: {
84
- title: string;
85
- description?: Array<string>;
86
- },
83
+ metadata: CommandMetadata,
87
84
  execution: Execution<Context, Payload>,
88
85
  subcommands: { [subcommand: Lowercase<string>]: Command<Payload, Result> },
89
86
  ): Command<Context, Result> {
90
87
  return {
91
- getTitle() {
92
- return metadata.title;
88
+ getDescription() {
89
+ return metadata.description;
93
90
  },
94
- prepareRunner(readerTokenizer: ReaderTokenizer) {
91
+ buildInterpreter(readerTokenizer: ReaderTokenizer) {
95
92
  try {
96
- const executionResolver = execution.prepareResolver(readerTokenizer);
93
+ const executionResolver = execution.createResolver(readerTokenizer);
97
94
  const subcommandName = readerTokenizer.consumePositional();
98
95
  if (subcommandName === undefined) {
99
96
  throw new Error("Expected a subcommand");
@@ -103,19 +100,20 @@ export function commandWithSubcommands<Context, Payload, Result>(
103
100
  if (subcommandInput === undefined) {
104
101
  throw new Error(`Unknown subcommand: ${subcommandName}`);
105
102
  }
106
- const subcommandRunner = subcommandInput.prepareRunner(readerTokenizer);
103
+ const subcommandInterpreter =
104
+ subcommandInput.buildInterpreter(readerTokenizer);
107
105
  const executionCallback = executionResolver();
108
106
  return {
109
107
  computeUsage() {
110
108
  const executionUsage = execution.computeUsage();
111
- const subcommandUsage = subcommandRunner.computeUsage();
109
+ const subcommandUsage = subcommandInterpreter.computeUsage();
112
110
  return {
113
111
  breadcrumbs: executionUsage.arguments
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,
@@ -125,7 +123,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
125
123
  },
126
124
  async execute(context: Context) {
127
125
  const payload = await executionCallback(context);
128
- return await subcommandRunner.execute(payload);
126
+ return await subcommandInterpreter.execute(payload);
129
127
  },
130
128
  };
131
129
  } catch (error) {
@@ -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
  }
@@ -4,7 +4,7 @@ import { ReaderTokenizer } from "./Reader";
4
4
 
5
5
  export type Execution<Context, Result> = {
6
6
  computeUsage(): ExecutionUsage;
7
- prepareResolver(
7
+ createResolver(
8
8
  readerTokenizer: ReaderTokenizer,
9
9
  ): ExecutionResolver<Context, Result>;
10
10
  };
@@ -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> {
@@ -57,7 +54,7 @@ export function execution<
57
54
  }
58
55
  return { options: optionsUsage, arguments: argumentsUsage };
59
56
  },
60
- prepareResolver(readerTokenizer: ReaderTokenizer) {
57
+ createResolver(readerTokenizer: ReaderTokenizer) {
61
58
  const optionsConsumers: any = {};
62
59
  for (const optionKey in inputs.options) {
63
60
  const optionInput = inputs.options[optionKey]!;
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/Option.ts CHANGED
@@ -11,6 +11,7 @@ export type OptionUsage = {
11
11
  long: Lowercase<string>; // TODO - better type for long option names ?
12
12
  short: string | undefined;
13
13
  label: Uppercase<string> | undefined;
14
+ // TODO - default value for usage ? but it can be dynamic, so maybe not
14
15
  };
15
16
 
16
17
  export type OptionConsumer<Value> = () => Value;
@@ -65,6 +66,7 @@ export function optionRepeatable<Value>(definition: {
65
66
  }): Option<Array<Value>> {
66
67
  return {
67
68
  generateUsage() {
69
+ // TODO - showcase that it can be repeated ?
68
70
  return {
69
71
  description: definition.description,
70
72
  long: definition.long,
package/src/lib/Reader.ts CHANGED
@@ -3,7 +3,7 @@ export type ReaderPositionals = {
3
3
  };
4
4
 
5
5
  export class ReaderTokenizer {
6
- #parsedArgv: Array<string>;
6
+ #parsedArgs: Array<string>;
7
7
  #parsedIndex: number;
8
8
  #parsedDouble: boolean;
9
9
 
@@ -17,8 +17,8 @@ export class ReaderTokenizer {
17
17
  #optionInfoByKey: Map<string, {}>; // TODO - what dis for
18
18
  #optionResultByKey: Map<string, Array<string> | null>;
19
19
 
20
- constructor(argv: Array<string>) {
21
- this.#parsedArgv = argv;
20
+ constructor(args: Array<string>) {
21
+ this.#parsedArgs = args;
22
22
  this.#parsedIndex = 0;
23
23
  this.#parsedDouble = false;
24
24
 
@@ -116,7 +116,7 @@ export class ReaderTokenizer {
116
116
  }
117
117
 
118
118
  #consumeArg(): string | null {
119
- const arg = this.#parsedArgv[this.#parsedIndex];
119
+ const arg = this.#parsedArgs[this.#parsedIndex];
120
120
  if (arg === undefined) {
121
121
  return null;
122
122
  }
package/src/lib/Run.ts CHANGED
@@ -1,32 +1,38 @@
1
- import { Command } from "./Command";
1
+ import { Command, CommandInterpreter } from "./Command";
2
2
  import { ReaderTokenizer } from "./Reader";
3
- import { typoInferSupport } from "./Typo";
3
+ import { typoInferProcessSupport, TypoSupport } from "./Typo";
4
4
  import { usageToPrintableLines } from "./Usage";
5
5
 
6
- export async function runWithArgv<Context, Result>(
7
- argv: string[],
6
+ export async function runAndExit<Context>(
7
+ cliName: Lowercase<string>,
8
+ cliArgs: Array<string>,
8
9
  context: Context,
9
- command: Command<Context, Result>,
10
- cliInfo?: {
11
- name?: Lowercase<string>;
12
- version?: string;
13
- helpOnError?: boolean;
10
+ command: Command<Context, void>,
11
+ application?: {
12
+ usageOnError?: boolean;
13
+ usageOnHelp?: boolean;
14
+ buildVersion?: string;
15
+ useColors?: boolean;
16
+ onMessage?: (message: string) => void;
17
+ onError?: (error: any) => void;
18
+ onExit?: (code: number) => never;
14
19
  },
15
- ): Promise<Result> {
16
- const cliName = cliInfo?.name ?? argv[1]!;
17
- const readerTokenizer = new ReaderTokenizer(argv.slice(2));
18
- if (cliInfo?.version) {
20
+ ): Promise<never> {
21
+ const readerTokenizer = new ReaderTokenizer(cliArgs);
22
+ if (application?.buildVersion) {
19
23
  readerTokenizer.registerFlag({
20
24
  key: "version",
21
25
  shorts: [],
22
26
  longs: ["version"],
23
27
  });
24
28
  }
25
- readerTokenizer.registerFlag({
26
- key: "help",
27
- shorts: [],
28
- longs: ["help"],
29
- });
29
+ if (application?.usageOnHelp ?? true) {
30
+ readerTokenizer.registerFlag({
31
+ key: "help",
32
+ shorts: [],
33
+ longs: ["help"],
34
+ });
35
+ }
30
36
  /*
31
37
  // TODO - handle completions ?
32
38
  readerTokenizer.registerFlag({
@@ -35,41 +41,50 @@ export async function runWithArgv<Context, Result>(
35
41
  longs: ["completion"],
36
42
  });
37
43
  */
38
- try {
39
- const commandRunner = command.prepareRunner(readerTokenizer);
40
- if (cliInfo?.version) {
41
- if (readerTokenizer.consumeFlag("version")) {
42
- console.log(cliName, cliInfo.version);
43
- process.exit(0);
44
- }
45
- }
46
- if (readerTokenizer.consumeFlag("help")) {
47
- console.log(
48
- usageToPrintableLines({
49
- cliName,
50
- commandUsage: commandRunner.computeUsage(),
51
- typoSupport: typoInferSupport(),
52
- }).join("\n"),
44
+ const commandInterpreter = command.buildInterpreter(readerTokenizer);
45
+ if (application?.buildVersion) {
46
+ if (readerTokenizer.consumeFlag("version")) {
47
+ (application?.onMessage ?? console.log)(
48
+ [cliName, application.buildVersion].join(" "),
53
49
  );
54
- process.exit(0);
50
+ return (application?.onExit ?? process.exit)(0);
55
51
  }
56
- try {
57
- return await commandRunner.execute(context);
58
- } catch (error) {
59
- if (cliInfo?.helpOnError ?? true) {
60
- console.log(
61
- usageToPrintableLines({
62
- cliName,
63
- commandUsage: commandRunner.computeUsage(),
64
- typoSupport: typoInferSupport(),
65
- }).join("\n"),
66
- );
67
- }
68
- console.error(error);
69
- process.exit(1);
52
+ }
53
+ if (application?.usageOnHelp ?? true) {
54
+ if (readerTokenizer.consumeFlag("help")) {
55
+ logUsageMessage(cliName, commandInterpreter, application);
56
+ return (application?.onExit ?? process.exit)(0);
70
57
  }
58
+ }
59
+ try {
60
+ await commandInterpreter.execute(context);
61
+ return (application?.onExit ?? process.exit)(0);
71
62
  } catch (error) {
72
- console.error(error);
73
- process.exit(1);
63
+ if (application?.usageOnError ?? true) {
64
+ logUsageMessage(cliName, commandInterpreter, application);
65
+ }
66
+ (application?.onError ?? console.error)(error);
67
+ return (application?.onExit ?? process.exit)(1);
68
+ }
69
+ }
70
+
71
+ function logUsageMessage<Context, Result>(
72
+ cliName: Lowercase<string>,
73
+ commandInterpreter: CommandInterpreter<Context, Result>,
74
+ application?: { useColors?: boolean; onMessage?: (message: string) => void },
75
+ ) {
76
+ (application?.onMessage ?? console.log)(
77
+ usageToPrintableLines({
78
+ cliName,
79
+ commandUsage: commandInterpreter.computeUsage(),
80
+ typoSupport: chooseTypoSupport(application?.useColors),
81
+ }).join("\n"),
82
+ );
83
+ }
84
+
85
+ function chooseTypoSupport(useColors?: boolean): TypoSupport {
86
+ if (useColors === undefined) {
87
+ return typoInferProcessSupport();
74
88
  }
89
+ return useColors ? "tty" : "none";
75
90
  }
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
@@ -42,7 +42,10 @@ export function typoPrintableString(
42
42
  throw new Error(`Unknown typo support: ${typoSupport}`);
43
43
  }
44
44
 
45
- export function typoInferSupport(): TypoSupport {
45
+ export function typoInferProcessSupport(): TypoSupport {
46
+ if (!process) {
47
+ return "none";
48
+ }
46
49
  if (process.env) {
47
50
  if (process.env["FORCE_COLOR"] === "0") {
48
51
  return "none";
@@ -54,7 +57,7 @@ export function typoInferSupport(): TypoSupport {
54
57
  return "none";
55
58
  }
56
59
  }
57
- if (!process || !process.stdout || !process.stdout.isTTY) {
60
+ if (!process.stdout || !process.stdout.isTTY) {
58
61
  return "none";
59
62
  }
60
63
  return "tty";
@@ -63,11 +66,20 @@ export function typoInferSupport(): TypoSupport {
63
66
  const resetCode = "\x1b[0m";
64
67
  const boldCode = "\x1b[1m";
65
68
  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",
69
+ darkBlack: "\x1b[30m",
70
+ darkRed: "\x1b[31m",
71
+ darkGreen: "\x1b[32m",
72
+ darkYellow: "\x1b[33m",
73
+ darkBlue: "\x1b[34m",
74
+ darkMagenta: "\x1b[35m",
75
+ darkCyan: "\x1b[36m",
76
+ darkWhite: "\x1b[37m",
77
+ brightBlack: "\x1b[90m",
78
+ brightRed: "\x1b[91m",
79
+ brightGreen: "\x1b[92m",
80
+ brightYellow: "\x1b[93m",
81
+ brightBlue: "\x1b[94m",
82
+ brightMagenta: "\x1b[95m",
83
+ brightCyan: "\x1b[96m",
84
+ brightWhite: "\x1b[97m",
73
85
  };