cli-kiss 0.0.1 → 0.0.2

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.
@@ -1,10 +1,10 @@
1
1
  import { ArgumentUsage } from "./Argument";
2
2
  import { OptionUsage } from "./Option";
3
- import { Processor } from "./Processor";
3
+ import { Process } from "./Process";
4
4
  import { ReaderTokenizer } from "./Reader";
5
5
 
6
6
  export type Command<Context, Result> = {
7
- getDescription(): string | undefined;
7
+ getTitle(): string | undefined;
8
8
  prepareRunner(
9
9
  readerTokenizer: ReaderTokenizer,
10
10
  ): CommandRunner<Context, Result>;
@@ -16,48 +16,55 @@ export type CommandRunner<Context, Result> = {
16
16
  };
17
17
 
18
18
  export type CommandUsage = {
19
- breadcrumbs: Array<string>;
20
- description: string | undefined;
19
+ breadcrumbs: Array<CommandUsageBreadcrumb>;
20
+ title: string;
21
+ description: Array<string> | undefined;
21
22
  options: Array<OptionUsage>;
22
23
  arguments: Array<ArgumentUsage>;
23
- subcommands: Array<{
24
- name: string;
25
- description: string | undefined;
26
- }>;
24
+ subcommands: Array<{ name: string; title: string | undefined }>;
25
+ };
26
+
27
+ export type CommandUsageBreadcrumb = {
28
+ kind: "command" | "argument";
29
+ value: string;
27
30
  };
28
31
 
29
32
  export function command<Context, Result>(
30
- description: string,
31
- processor: Processor<Context, Result>,
33
+ metadata: {
34
+ title: string;
35
+ description?: Array<string>;
36
+ },
37
+ process: Process<Context, Result>,
32
38
  ): Command<Context, Result> {
33
39
  return {
34
- getDescription() {
35
- return description;
40
+ getTitle() {
41
+ return metadata.title;
36
42
  },
37
43
  prepareRunner(readerTokenizer: ReaderTokenizer) {
38
44
  function computeUsage(): CommandUsage {
39
- const processorUsage = processor.computeUsage();
45
+ const processUsage = process.computeUsage();
40
46
  return {
41
- breadcrumbs: processorUsage.arguments.map(
42
- (argument) => argument.label,
47
+ breadcrumbs: processUsage.arguments.map((argument) =>
48
+ breadcrumbArgument(argument.label),
43
49
  ),
44
- description,
45
- options: processorUsage.options,
46
- arguments: processorUsage.arguments,
50
+ title: metadata.title,
51
+ description: metadata.description,
52
+ options: processUsage.options,
53
+ arguments: processUsage.arguments,
47
54
  subcommands: [],
48
55
  };
49
56
  }
50
57
  try {
51
- const processorResolver = processor.prepareResolver(readerTokenizer);
58
+ const processResolver = process.prepareResolver(readerTokenizer);
52
59
  const lastPositional = readerTokenizer.consumePositional();
53
60
  if (lastPositional !== undefined) {
54
61
  throw Error(`Unprocessed positional: ${lastPositional}`);
55
62
  }
56
- const processorRunner = processorResolver();
63
+ const processRunner = processResolver();
57
64
  return {
58
65
  computeUsage,
59
66
  async execute(context: Context) {
60
- return await processorRunner.execute(context);
67
+ return await processRunner.execute(context);
61
68
  },
62
69
  };
63
70
  } catch (error) {
@@ -73,17 +80,20 @@ export function command<Context, Result>(
73
80
  }
74
81
 
75
82
  export function commandWithSubcommands<Context, Payload, Result>(
76
- description: string,
77
- processor: Processor<Context, Payload>,
83
+ metadata: {
84
+ title: string;
85
+ description?: Array<string>;
86
+ },
87
+ process: Process<Context, Payload>,
78
88
  subcommands: { [subcommand: Lowercase<string>]: Command<Payload, Result> },
79
89
  ): Command<Context, Result> {
80
90
  return {
81
- getDescription() {
82
- return description;
91
+ getTitle() {
92
+ return metadata.title;
83
93
  },
84
94
  prepareRunner(readerTokenizer: ReaderTokenizer) {
85
95
  try {
86
- const processorResolver = processor.prepareResolver(readerTokenizer);
96
+ const processResolver = process.prepareResolver(readerTokenizer);
87
97
  const subcommandName = readerTokenizer.consumePositional();
88
98
  if (subcommandName === undefined) {
89
99
  throw new Error("Expected a subcommand");
@@ -94,44 +104,46 @@ export function commandWithSubcommands<Context, Payload, Result>(
94
104
  throw new Error(`Unknown subcommand: ${subcommandName}`);
95
105
  }
96
106
  const subcommandRunner = subcommandInput.prepareRunner(readerTokenizer);
97
- const processorRunner = processorResolver();
107
+ const processRunner = processResolver();
98
108
  return {
99
109
  computeUsage() {
100
- const processorUsage = processor.computeUsage();
110
+ const processUsage = process.computeUsage();
101
111
  const subcommandUsage = subcommandRunner.computeUsage();
102
112
  return {
103
- breadcrumbs: processorUsage.arguments
104
- .map((argument) => argument.label)
105
- .concat([subcommandName])
113
+ breadcrumbs: processUsage.arguments
114
+ .map((argument) => breadcrumbArgument(argument.label))
115
+ .concat([breadcrumbCommand(subcommandName)])
106
116
  .concat(subcommandUsage.breadcrumbs),
117
+ title: subcommandUsage.title,
107
118
  description: subcommandUsage.description,
108
- options: processorUsage.options.concat(subcommandUsage.options),
109
- arguments: processorUsage.arguments.concat(
119
+ options: processUsage.options.concat(subcommandUsage.options),
120
+ arguments: processUsage.arguments.concat(
110
121
  subcommandUsage.arguments,
111
122
  ),
112
123
  subcommands: subcommandUsage.subcommands,
113
124
  };
114
125
  },
115
126
  async execute(context: Context) {
116
- const payload = await processorRunner.execute(context);
127
+ const payload = await processRunner.execute(context);
117
128
  return await subcommandRunner.execute(payload);
118
129
  },
119
130
  };
120
131
  } catch (error) {
121
132
  return {
122
133
  computeUsage() {
123
- const processorUsage = processor.computeUsage();
134
+ const processUsage = process.computeUsage();
124
135
  return {
125
- breadcrumbs: processorUsage.arguments
126
- .map((argument) => argument.label)
127
- .concat(["<SUBCOMMAND>"]),
128
- description,
129
- options: processorUsage.options,
130
- arguments: processorUsage.arguments,
136
+ breadcrumbs: processUsage.arguments
137
+ .map((argument) => breadcrumbArgument(argument.label))
138
+ .concat([breadcrumbCommand("<SUBCOMMAND>")]),
139
+ title: metadata.title,
140
+ description: metadata.description,
141
+ options: processUsage.options,
142
+ arguments: processUsage.arguments,
131
143
  subcommands: Object.entries(subcommands).map(
132
144
  ([name, subcommand]) => ({
133
145
  name,
134
- description: subcommand.getDescription(),
146
+ title: subcommand.getTitle(),
135
147
  }),
136
148
  ),
137
149
  };
@@ -144,3 +156,11 @@ export function commandWithSubcommands<Context, Payload, Result>(
144
156
  },
145
157
  };
146
158
  }
159
+
160
+ function breadcrumbArgument(label: string): CommandUsageBreadcrumb {
161
+ return { kind: "argument", value: label };
162
+ }
163
+
164
+ function breadcrumbCommand(name: string): CommandUsageBreadcrumb {
165
+ return { kind: "command", value: name };
166
+ }
@@ -0,0 +1,56 @@
1
+ import { TypoSupport, TypoText, typoPrintableString } from "./Typo";
2
+
3
+ export type Grid = Array<GridRow>;
4
+ export type GridRow = Array<GridCell>;
5
+ export type GridCell = Array<TypoText>;
6
+
7
+ export function gridToPrintableLines(grid: Grid, typoSupport: TypoSupport) {
8
+ const lines = new Array<string>();
9
+ const gridWidths = new Array<number>();
10
+ for (const gridRow of grid) {
11
+ for (
12
+ let gridColumnIndex = 0;
13
+ gridColumnIndex < gridRow.length;
14
+ gridColumnIndex++
15
+ ) {
16
+ const gridCell = gridRow[gridColumnIndex]!;
17
+ const length = gridCellLength(gridCell);
18
+ if (
19
+ gridWidths[gridColumnIndex] === undefined ||
20
+ length > gridWidths[gridColumnIndex]!
21
+ ) {
22
+ gridWidths[gridColumnIndex] = length;
23
+ }
24
+ }
25
+ }
26
+ for (const gridRow of grid) {
27
+ const lineColumns = new Array<string>();
28
+ for (
29
+ let gridColumnIndex = 0;
30
+ gridColumnIndex < gridRow.length;
31
+ gridColumnIndex++
32
+ ) {
33
+ const gridCell = gridRow[gridColumnIndex]!;
34
+ const parts = gridCell.map((text) =>
35
+ typoPrintableString(typoSupport, text),
36
+ );
37
+ if (gridColumnIndex < gridRow.length - 1) {
38
+ const length = gridCellLength(gridCell);
39
+ const padding = " ".repeat(gridWidths[gridColumnIndex]! - length);
40
+ lineColumns.push(parts.join("") + padding);
41
+ } else {
42
+ lineColumns.push(parts.join(""));
43
+ }
44
+ }
45
+ lines.push(lineColumns.join(" "));
46
+ }
47
+ return lines;
48
+ }
49
+
50
+ function gridCellLength(cell: GridCell): number {
51
+ let length = 0;
52
+ for (const text of cell) {
53
+ length += text.value.length;
54
+ }
55
+ return length;
56
+ }
package/src/lib/Option.ts CHANGED
@@ -122,7 +122,7 @@ export function optionSingleValue<Value>(definition: {
122
122
  }
123
123
  readerTokenizer.registerOption({ key, longs, shorts });
124
124
  return () => {
125
- // TODO - error handling
125
+ // TODO - smooth and beautiful error handling
126
126
  const values = readerTokenizer.consumeOption(definition.long);
127
127
  if (values.length > 1) {
128
128
  throw new Error(
@@ -2,28 +2,28 @@ import { Argument, ArgumentUsage } from "./Argument";
2
2
  import { Option, OptionUsage } from "./Option";
3
3
  import { ReaderTokenizer } from "./Reader";
4
4
 
5
- export type Processor<Context, Result> = {
6
- computeUsage(): ProcessorUsage;
5
+ export type Process<Context, Result> = {
6
+ computeUsage(): ProcessUsage;
7
7
  prepareResolver(
8
8
  readerTokenizer: ReaderTokenizer,
9
- ): ProcessorResolver<Context, Result>;
9
+ ): ProcessResolver<Context, Result>;
10
10
  };
11
11
 
12
- export type ProcessorResolver<Context, Result> = () => ProcessorRunner<
12
+ export type ProcessResolver<Context, Result> = () => ProcessRunner<
13
13
  Context,
14
14
  Result
15
15
  >;
16
16
 
17
- export type ProcessorRunner<Context, Result> = {
17
+ export type ProcessRunner<Context, Result> = {
18
18
  execute(context: Context): Promise<Result>;
19
19
  };
20
20
 
21
- export type ProcessorUsage = {
21
+ export type ProcessUsage = {
22
22
  options: Array<OptionUsage>;
23
23
  arguments: Array<ArgumentUsage>;
24
24
  };
25
25
 
26
- export function processor<
26
+ export function process<
27
27
  Context,
28
28
  Result,
29
29
  Options extends { [option: string]: Option<any> },
@@ -43,7 +43,7 @@ export function processor<
43
43
  };
44
44
  },
45
45
  ) => Promise<Result>,
46
- ): Processor<Context, Result> {
46
+ ): Process<Context, Result> {
47
47
  return {
48
48
  computeUsage() {
49
49
  const optionsUsage = new Array<OptionUsage>();
package/src/lib/Reader.ts CHANGED
@@ -187,11 +187,9 @@ export class ReaderTokenizer {
187
187
  const flagKey = this.#flagKeyByLong.get(long);
188
188
  if (flagKey !== undefined) {
189
189
  if (direct !== null) {
190
- if (direct === "true") {
191
- return this.#acknowledgeFlag(flagKey, true);
192
- }
193
- if (direct === "false") {
194
- return this.#acknowledgeFlag(flagKey, false);
190
+ const value = asBoolean(direct);
191
+ if (value !== undefined) {
192
+ return this.#acknowledgeFlag(flagKey, value);
195
193
  }
196
194
  throw new Error(
197
195
  `Invalid parameter for long flag: ${flagKey}, value: ${direct}`,
@@ -213,12 +211,9 @@ export class ReaderTokenizer {
213
211
  const flagKey = this.#flagKeyByShort.get(short);
214
212
  if (flagKey !== undefined) {
215
213
  if (rest.startsWith("=")) {
216
- if (rest === "=true") {
217
- this.#acknowledgeFlag(flagKey, true);
218
- return true;
219
- }
220
- if (rest === "=false") {
221
- this.#acknowledgeFlag(flagKey, false);
214
+ const value = asBoolean(rest.slice(1));
215
+ if (value !== undefined) {
216
+ this.#acknowledgeFlag(flagKey, value);
222
217
  return true;
223
218
  }
224
219
  throw new Error(
@@ -283,3 +278,14 @@ export class ReaderTokenizer {
283
278
  }
284
279
  }
285
280
  }
281
+
282
+ function asBoolean(value: string): boolean | undefined {
283
+ const lower = value.toLowerCase();
284
+ if (lower === "true" || lower === "t" || lower === "y" || lower === "yes") {
285
+ return true;
286
+ }
287
+ if (lower === "false" || lower === "f" || lower === "n" || lower === "no") {
288
+ return false;
289
+ }
290
+ return undefined;
291
+ }
package/src/lib/Run.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  import { Command } from "./Command";
2
2
  import { ReaderTokenizer } from "./Reader";
3
- import { usageFormatter } from "./Usage";
3
+ import { typoInferSupport } from "./Typo";
4
+ import { usageToPrintableLines } from "./Usage";
4
5
 
5
6
  export async function runWithArgv<Context, Result>(
6
7
  argv: string[],
7
8
  context: Context,
8
9
  command: Command<Context, Result>,
9
- cliInfo?: { name?: string; version?: string; helpOnError?: boolean },
10
+ cliInfo?: {
11
+ name?: Lowercase<string>;
12
+ version?: string;
13
+ helpOnError?: boolean;
14
+ },
10
15
  ): Promise<Result> {
11
16
  const cliName = cliInfo?.name ?? argv[1]!;
12
17
  const readerTokenizer = new ReaderTokenizer(argv.slice(2));
13
- readerTokenizer.registerFlag({
14
- key: "help",
15
- shorts: [],
16
- longs: ["help"],
17
- });
18
18
  if (cliInfo?.version) {
19
19
  readerTokenizer.registerFlag({
20
20
  key: "version",
@@ -22,6 +22,11 @@ export async function runWithArgv<Context, Result>(
22
22
  longs: ["version"],
23
23
  });
24
24
  }
25
+ readerTokenizer.registerFlag({
26
+ key: "help",
27
+ shorts: [],
28
+ longs: ["help"],
29
+ });
25
30
  /*
26
31
  // TODO - handle completions ?
27
32
  readerTokenizer.registerFlag({
@@ -39,14 +44,26 @@ export async function runWithArgv<Context, Result>(
39
44
  }
40
45
  }
41
46
  if (readerTokenizer.consumeFlag("help")) {
42
- console.log(usageFormatter(cliName, commandRunner.computeUsage()));
47
+ console.log(
48
+ usageToPrintableLines({
49
+ cliName,
50
+ commandUsage: commandRunner.computeUsage(),
51
+ typoSupport: typoInferSupport(),
52
+ }).join("\n"),
53
+ );
43
54
  process.exit(0);
44
55
  }
45
56
  try {
46
57
  return await commandRunner.execute(context);
47
58
  } catch (error) {
48
59
  if (cliInfo?.helpOnError ?? true) {
49
- console.log(usageFormatter(cliName, commandRunner.computeUsage()));
60
+ console.log(
61
+ usageToPrintableLines({
62
+ cliName,
63
+ commandUsage: commandRunner.computeUsage(),
64
+ typoSupport: typoInferSupport(),
65
+ }).join("\n"),
66
+ );
50
67
  }
51
68
  console.error(error);
52
69
  process.exit(1);
@@ -0,0 +1,73 @@
1
+ export type TypoSupport = "none" | "tty" | "html" | "mock";
2
+
3
+ export type TypoText = {
4
+ value: string;
5
+ color?: keyof typeof colorCodes;
6
+ bold?: boolean;
7
+ };
8
+
9
+ export function typoPrintableString(
10
+ typoSupport: TypoSupport,
11
+ typoText: TypoText,
12
+ ): string {
13
+ if (typoSupport === "none") {
14
+ return typoText.value;
15
+ }
16
+ if (typoSupport === "tty") {
17
+ const colorStartCode = typoText.color ? colorCodes[typoText.color] : "";
18
+ const colorBoldCode = typoText.bold ? boldCode : "";
19
+ return `${colorStartCode}${colorBoldCode}${typoText.value}${resetCode}`;
20
+ }
21
+ if (typoSupport === "html") {
22
+ const colorStartTag = typoText.color
23
+ ? `<span style="color: ${typoText.color}">`
24
+ : "";
25
+ const colorEndTag = typoText.color ? "</span>" : "";
26
+ const boldStartTag = typoText.bold ? "<b>" : "";
27
+ const boldEndTag = typoText.bold ? "</b>" : "";
28
+ return `${colorStartTag}${boldStartTag}${typoText.value}${boldEndTag}${colorEndTag}`;
29
+ }
30
+ if (typoSupport === "mock") {
31
+ if (typoText.color && typoText.bold) {
32
+ return `{${typoText.value}}@${typoText.color}+`;
33
+ }
34
+ if (typoText.color) {
35
+ return `{${typoText.value}}@${typoText.color}`;
36
+ }
37
+ if (typoText.bold) {
38
+ return `{${typoText.value}}+`;
39
+ }
40
+ return `{${typoText.value}}`;
41
+ }
42
+ throw new Error(`Unknown typo support: ${typoSupport}`);
43
+ }
44
+
45
+ export function typoInferSupport(): TypoSupport {
46
+ if (process.env) {
47
+ if (process.env["FORCE_COLOR"] === "0") {
48
+ return "none";
49
+ }
50
+ if (process.env["FORCE_COLOR"]) {
51
+ return "tty";
52
+ }
53
+ if ("NO_COLOR" in process.env) {
54
+ return "none";
55
+ }
56
+ }
57
+ if (!process || !process.stdout || !process.stdout.isTTY) {
58
+ return "none";
59
+ }
60
+ return "tty";
61
+ }
62
+
63
+ const resetCode = "\x1b[0m";
64
+ const boldCode = "\x1b[1m";
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",
73
+ };