@procedures/cli-kit 0.0.0-stage → 0.35.1

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/src/help.ts ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * The help text of a command line: the help of one command, of a group of commands and of the
3
+ * whole program.
4
+ *
5
+ * Each function gives the text and prints nothing. The caller prints it.
6
+ */
7
+
8
+ import type { GlobalFlag } from "./args.js";
9
+ import { findChildren } from "./args.js";
10
+ import {
11
+ extractSchemaFields,
12
+ fieldWithoutSchema,
13
+ toKebab,
14
+ typeName,
15
+ type CLIMeta,
16
+ type CommandLike,
17
+ type SchemaFieldInfo,
18
+ } from "./fields.js";
19
+
20
+ /** The options of the help text. */
21
+ export interface HelpOptions {
22
+ /** The name of the program, as a person types it, for example `mark`. */
23
+ name: string;
24
+ /** The global flags that the help lists. The help lists each flag that is not `hidden`. */
25
+ globals?: readonly GlobalFlag[] | undefined;
26
+ }
27
+
28
+ /** This function gives the hint of a field on the command line, for the help text. */
29
+ function valueHint(field: SchemaFieldInfo): string {
30
+ const type = field.fieldType;
31
+ switch (type.kind) {
32
+ case "boolean":
33
+ return "";
34
+ case "array":
35
+ return ` <${typeName(type.item ?? { kind: "any" })}>... (repeat the flag, or give a JSON array)`;
36
+ case "record":
37
+ return " <key=value>... (repeat the flag, or give a JSON object)";
38
+ case "object":
39
+ case "any":
40
+ return " <json>";
41
+ case "enum":
42
+ return ` <${(field.enumValues ?? []).join("|")}>`;
43
+ default:
44
+ return ` <${typeName(type)}>`;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * This function gives the help text of one command. `schema` is the input schema, or the fields
50
+ * of the input. `result` gives the fields of the result. The help lists them after the options.
51
+ */
52
+ export function generateHelp(
53
+ path: readonly string[],
54
+ meta: CLIMeta,
55
+ schema: unknown,
56
+ result: readonly SchemaFieldInfo[],
57
+ options: HelpOptions
58
+ ): string {
59
+ const positionalArgs = meta.args ?? [];
60
+ const shorts = meta.shorts ?? {};
61
+
62
+ const lines: string[] = [];
63
+
64
+ const schemaFields: SchemaFieldInfo[] = Array.isArray(schema)
65
+ ? (schema as SchemaFieldInfo[])
66
+ : schema
67
+ ? extractSchemaFields(schema)
68
+ : [];
69
+ const schemaFieldMap = new Map(schemaFields.map((f) => [f.name, f]));
70
+
71
+ // Command signature: the last positional array takes the rest of the arguments
72
+ const cmdName = `${options.name} ${path.join(" ")}`;
73
+ const posStr = positionalArgs
74
+ .map((a, i) => {
75
+ const many = i === positionalArgs.length - 1 && schemaFieldMap.get(a)?.fieldType.kind === "array";
76
+ return many ? `<${a}...>` : `<${a}>`;
77
+ })
78
+ .join(" ");
79
+ lines.push(`Usage: ${cmdName}${posStr ? " " + posStr : ""} [options]`);
80
+ lines.push("");
81
+
82
+ if (meta.description) {
83
+ lines.push(meta.description);
84
+ lines.push("");
85
+ }
86
+
87
+ if (positionalArgs.length > 0) {
88
+ lines.push("Arguments:");
89
+ for (const arg of positionalArgs) {
90
+ const fieldInfo = schemaFieldMap.get(arg);
91
+ const typeStr = fieldInfo ? ` (${fieldInfo.type})` : "";
92
+ const reqStr = fieldInfo?.required ? " [required]" : "";
93
+ const descStr = fieldInfo?.description ? ` ${fieldInfo.description}` : "";
94
+ lines.push(` ${arg}${typeStr}${reqStr}`);
95
+ if (descStr) {
96
+ lines.push(` ${descStr}`);
97
+ }
98
+ }
99
+ lines.push("");
100
+ }
101
+
102
+ // Options: each schema field that is not positional, with its short flag if it has one
103
+ const optionLines: string[] = [];
104
+ const positionalSet = new Set(positionalArgs);
105
+ const listed = new Set<string>();
106
+ const optionFields = [
107
+ ...Object.keys(shorts).map((name) => schemaFieldMap.get(name) ?? fieldWithoutSchema(name)),
108
+ ...schemaFields.filter((field) => !(field.name in shorts) && !positionalSet.has(field.name)),
109
+ ];
110
+ for (const field of optionFields) {
111
+ if (listed.has(field.name)) {
112
+ continue;
113
+ }
114
+ listed.add(field.name);
115
+ const short = shorts[field.name];
116
+ const kebab = toKebab(field.name);
117
+ const flag = short ? ` -${short}, --${kebab}` : ` --${kebab}`;
118
+ const negation = field.fieldType.kind === "boolean" ? ` (or --no-${kebab})` : "";
119
+ const reqStr = field.required ? " [required]" : "";
120
+ const defaultStr = field.defaultValue !== undefined ? ` (default: ${JSON.stringify(field.defaultValue)})` : "";
121
+ optionLines.push(`${flag}${valueHint(field)}${negation}${reqStr}${defaultStr}`);
122
+ if (field.description) {
123
+ optionLines.push(` ${field.description}`);
124
+ }
125
+ }
126
+
127
+ if (optionLines.length > 0) {
128
+ lines.push("Options:");
129
+ lines.push(...optionLines);
130
+ }
131
+
132
+ if (result.length > 0) {
133
+ lines.push("");
134
+ lines.push("Result:");
135
+ for (const field of result) {
136
+ const optional = field.required ? "" : ", optional";
137
+ lines.push(` ${field.name} (${field.type}${optional})`);
138
+ if (field.description) {
139
+ lines.push(` ${field.description}`);
140
+ }
141
+ }
142
+ }
143
+
144
+ const globals = (options.globals ?? []).filter((flag) => !flag.hidden);
145
+ if (globals.length > 0) {
146
+ lines.push("");
147
+ lines.push("Global Options (before the command, or after it when the command has no flag of that name):");
148
+ for (const flag of globals) {
149
+ const name = flag.short ? `-${flag.short}, --${flag.long}` : ` --${flag.long}`;
150
+ lines.push(` ${name.padEnd(18)} ${flag.description}`);
151
+ }
152
+ }
153
+
154
+ return lines.join("\n");
155
+ }
156
+
157
+ /**
158
+ * This function gives the help of a group of commands: one message for each line. It gives
159
+ * undefined when no command is in the group. A message can contain a line break.
160
+ */
161
+ export function groupHelp(name: string, commands: readonly CommandLike[], path: readonly string[]): string[] | undefined {
162
+ const children = findChildren(commands, path);
163
+ if (children.length === 0) {
164
+ return undefined;
165
+ }
166
+ const groupName = path.join(" ");
167
+ const lines = [`${groupName} commands:\n`];
168
+
169
+ // The commands directly in the group
170
+ for (const child of children.filter((p) => p.path.length === path.length + 1)) {
171
+ const meta = (child.metadata ?? {}) as CLIMeta;
172
+ const cmdName = child.path[child.path.length - 1] ?? "";
173
+ lines.push(` ${name} ${groupName} ${cmdName} ${meta.description ?? ""}`);
174
+ }
175
+
176
+ // The groups in the group
177
+ const childGroups = new Set<string>();
178
+ for (const child of children) {
179
+ if (child.path.length > path.length + 1) {
180
+ const groupSeg = child.path[path.length];
181
+ if (groupSeg) childGroups.add(groupSeg);
182
+ }
183
+ }
184
+ for (const group of childGroups) {
185
+ lines.push(` ${name} ${groupName} ${group} ${group} commands`);
186
+ }
187
+
188
+ lines.push(`\nRun '${name} ${groupName} <command> --help' for more info.`);
189
+ return lines;
190
+ }
191
+
192
+ /** The options of the help of the whole program. */
193
+ export interface RootHelpOptions {
194
+ /** The name of the program. */
195
+ name: string;
196
+ /** The first line of the help. The default is the name. */
197
+ title?: string | undefined;
198
+ /** The lines after the list of the commands. */
199
+ footer?: readonly string[] | undefined;
200
+ }
201
+
202
+ /**
203
+ * This function gives the help of the whole program: the commands by their first word. It gives
204
+ * one message for each line. A message can contain a line break.
205
+ */
206
+ export function rootHelp(commands: readonly CommandLike[], options: RootHelpOptions): string[] {
207
+ const { name } = options;
208
+ const lines = [`${options.title ?? name}\n`, "Commands:\n"];
209
+
210
+ const groups = new Map<string, CommandLike[]>();
211
+ for (const proc of commands) {
212
+ const group = proc.path[0] ?? "other";
213
+ const members = groups.get(group) ?? [];
214
+ members.push(proc);
215
+ groups.set(group, members);
216
+ }
217
+
218
+ for (const [group, procs] of groups) {
219
+ lines.push(` ${group}`);
220
+ for (const proc of procs) {
221
+ const meta = (proc.metadata ?? {}) as CLIMeta;
222
+ lines.push(` ${name} ${proc.path.join(" ")} ${meta.description ?? ""}`);
223
+ }
224
+ lines.push("");
225
+ }
226
+
227
+ lines.push(`Run '${name} <command> --help' for more information.`);
228
+ lines.push(...(options.footer ?? []));
229
+ return lines;
230
+ }
231
+
232
+ /** This function gives the message of a validation error: one line for each issue of a Zod error. */
233
+ export function validationMessage(error: unknown): string {
234
+ const issues = (error as { issues?: Array<{ path?: unknown[]; message?: string }> }).issues;
235
+ if (Array.isArray(issues) && issues.length > 0) {
236
+ return issues
237
+ .map((issue) => `${(issue.path ?? []).join(".") || "input"}: ${issue.message ?? "invalid"}`)
238
+ .join("\n ");
239
+ }
240
+ return error instanceof Error ? error.message : String(error);
241
+ }
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `@procedures/cli-kit`: a command line for any procedure registry.
3
+ *
4
+ * The package has no procedures, and an import registers nothing. `createProcedureCli()` makes
5
+ * a whole program. The other exports are its parts: the parse, the help, the questions and the
6
+ * output. The `mark` CLI uses the parts.
7
+ */
8
+
9
+ export {
10
+ createProcedureCli,
11
+ STANDARD_GLOBAL_FLAGS,
12
+ type CliCallOptions,
13
+ type CliClient,
14
+ type CommandSource,
15
+ type FormatContext,
16
+ type FormatHook,
17
+ type ProcedureCli,
18
+ type ProcedureCliOptions,
19
+ type ProcedureCliRunOptions,
20
+ } from "./cli.js";
21
+ export {
22
+ findChildren,
23
+ findGlobalFlag,
24
+ findProcedure,
25
+ parseCommandLine,
26
+ parseLeadingGlobals,
27
+ procedureFlags,
28
+ type GlobalFlag,
29
+ type Globals,
30
+ type ParsedCommandLine,
31
+ type ParseOptions,
32
+ } from "./args.js";
33
+ export {
34
+ convertValue,
35
+ extractSchemaFields,
36
+ fieldTypeOf,
37
+ fieldsOf,
38
+ mergeValue,
39
+ resultFieldsOf,
40
+ resultFieldsOfSchema,
41
+ toCamel,
42
+ toKebab,
43
+ type CLIMeta,
44
+ type CommandLike,
45
+ type Converted,
46
+ type FieldKind,
47
+ type FieldType,
48
+ type SchemaFieldInfo,
49
+ } from "./fields.js";
50
+ export {
51
+ generateHelp,
52
+ groupHelp,
53
+ rootHelp,
54
+ validationMessage,
55
+ type HelpOptions,
56
+ type RootHelpOptions,
57
+ } from "./help.js";
58
+ export {
59
+ askMissing,
60
+ canAsk,
61
+ missingFields,
62
+ processTerminal,
63
+ questionOf,
64
+ terminalAsker,
65
+ type Answer,
66
+ type Asked,
67
+ type Asker,
68
+ type Terminal,
69
+ } from "./ask.js";
70
+ export { formatOutput, plainPrint, type OutputFormat, type Print, type Spinner } from "./format.js";
71
+ export { failureOf } from "./failure.js";