Skip to content

Commit f7a3021

Browse files
committed
improve command help display
1 parent 3a7ea37 commit f7a3021

7 files changed

Lines changed: 249 additions & 64 deletions

File tree

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,7 @@ iterable users delete user@example.com # routes to delete-by-email
107107

108108
## Available Commands
109109

110-
See the [full command reference](COMMANDS.md) for all 109 commands with parameter details.
111-
112-
**Categories:** campaigns, catalogs, events, experiments, export, journeys, lists, messaging, snippets, subscriptions, templates, users, webhooks
110+
See the [full command reference](COMMANDS.md) for all commands with parameter details.
113111

114112
## Global Options
115113

src/index.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { z } from "zod";
77
import { createClient, loadCliConfig } from "./config.js";
88
import { CliError, UsageError } from "./errors.js";
99
import { formatOutput, getDefaultFormat } from "./output.js";
10-
import { buildParser, parseCommand } from "./parser.js";
10+
import { parseCommand } from "./parser.js";
1111
import {
1212
findCommand,
1313
parseArgs,
1414
showCategoryHelp,
15+
showCommandHelp,
1516
showGlobalHelp,
1617
showVersion,
1718
} from "./router.js";
@@ -56,22 +57,12 @@ async function main(): Promise<void> {
5657
const command = findCommand(parsed.category, parsed.action);
5758
if (!command) {
5859
throw new UsageError(
59-
`Unknown command: ${parsed.category} ${parsed.action}\nRun '${COMMAND_NAME} ${parsed.category} --help' to see available commands.`
60+
`Unknown command: ${parsed.category} ${parsed.action}`
6061
);
6162
}
6263

6364
if (parsed.globalFlags.help) {
64-
const built = buildParser(
65-
command.schema,
66-
`iterable ${parsed.category} ${parsed.action}`,
67-
command.positionalArgs,
68-
command.cliTransforms
69-
);
70-
try {
71-
built.showHelp();
72-
} catch {
73-
// zod-opts calls process.exit on --help
74-
}
65+
showCommandHelp(parsed.category, command);
7566
return;
7667
}
7768

@@ -147,8 +138,24 @@ main().catch(async (error: unknown) => {
147138
const err = (msg: string) => console.error(chalk.red(`✖ ${msg}`)); // eslint-disable-line no-console
148139
const hint = (msg: string) => console.error(chalk.dim(` ${msg}`)); // eslint-disable-line no-console
149140

141+
const helpHint = (): void => {
142+
try {
143+
const { category, action } = parseArgs(process.argv.slice(2));
144+
if (category && action && findCommand(category, action)) {
145+
hint(
146+
`Run '${COMMAND_NAME} ${category} ${action} --help' for usage details.`
147+
);
148+
return;
149+
}
150+
} catch {
151+
// Fall through to generic hint
152+
}
153+
hint(`Run '${COMMAND_NAME} --help' for usage details.`);
154+
};
155+
150156
if (error instanceof CliError) {
151157
err(error.message);
158+
helpHint();
152159
process.exit(error.exitCode);
153160
}
154161

@@ -157,6 +164,7 @@ main().catch(async (error: unknown) => {
157164
for (const issue of error.issues) {
158165
hint(`${issue.path.join(".")}: ${issue.message}`);
159166
}
167+
helpHint();
160168
process.exit(2);
161169
}
162170

src/output.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function findTableData(data: unknown): TableData | null {
7878
if (data && typeof data === "object" && !Array.isArray(data)) {
7979
const entries = Object.entries(data as Record<string, unknown>);
8080
for (const [arrayKey, value] of entries) {
81-
if (Array.isArray(value)) {
81+
if (isObjectArray(value)) {
8282
const metadata: Record<string, unknown> = {};
8383
for (const [key, val] of entries) {
8484
if (key !== arrayKey) metadata[key] = val;

src/parser.ts

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import { parser } from "zod-opts";
33

4-
import type { CliTransform, CommandDefinition } from "./commands/types.js";
4+
import type { CliTransform } from "./commands/types.js";
55
import { ValidationError } from "./errors.js";
66

77
function unwrapSchema(schema: z.ZodType): z.ZodType {
@@ -34,9 +34,9 @@ function coerceForZodOpts(fieldSchema: z.ZodType): z.ZodType {
3434
const element = unwrapSchema(inner.element as z.ZodType);
3535
if (element instanceof z.ZodEnum) {
3636
const coerced = z.array(z.string());
37-
return fieldSchema instanceof z.ZodOptional
38-
? coerced.optional()
39-
: coerced;
37+
if (fieldSchema instanceof z.ZodOptional) return coerced.optional();
38+
if (fieldSchema instanceof z.ZodDefault) return coerced.default([]);
39+
return coerced;
4040
}
4141
}
4242
return fieldSchema;
@@ -86,7 +86,9 @@ function getDefaultValue(
8686

8787
function isOptionalField(schema: z.ZodType): boolean {
8888
return (
89-
schema instanceof z.ZodOptional || getDefaultValue(schema) !== undefined
89+
schema instanceof z.ZodOptional ||
90+
schema instanceof z.ZodNullable ||
91+
getDefaultValue(schema) !== undefined
9092
);
9193
}
9294

@@ -128,8 +130,14 @@ export interface FieldInfo {
128130
objectKeys?: string[] | undefined;
129131
}
130132

133+
export interface DescribeCommandInput {
134+
schema: z.ZodType;
135+
positionalArgs?: string[] | undefined;
136+
cliTransforms?: Record<string, CliTransform> | undefined;
137+
}
138+
131139
/** Describe all fields of a command — single source of truth for --help and COMMANDS.md. */
132-
export function describeCommand(cmd: CommandDefinition): FieldInfo[] {
140+
export function describeCommand(cmd: DescribeCommandInput): FieldInfo[] {
133141
const shape = getSchemaShape(cmd.schema);
134142
const positionalFieldSet = new Set(cmd.positionalArgs ?? []);
135143
const transformFieldSet = new Set(Object.keys(cmd.cliTransforms ?? {}));
@@ -199,7 +207,6 @@ export function describeCommand(cmd: CommandDefinition): FieldInfo[] {
199207

200208
export interface BuiltParser {
201209
parse: (argv: string[]) => Record<string, unknown>;
202-
showHelp: () => void;
203210
jsonFallbackFields: string[];
204211
}
205212

@@ -212,12 +219,7 @@ export function buildParser(
212219
): BuiltParser {
213220
const shape = getSchemaShape(schema);
214221

215-
const cmd = {
216-
schema,
217-
positionalArgs,
218-
cliTransforms,
219-
} as CommandDefinition;
220-
const fields = describeCommand(cmd);
222+
const fields = describeCommand({ schema, positionalArgs, cliTransforms });
221223

222224
const options: Record<string, { type: z.ZodType }> = {};
223225
const jsonFallbackFields: string[] = [];
@@ -249,15 +251,6 @@ export function buildParser(
249251
}
250252
}
251253

252-
options["json"] = {
253-
type: z
254-
.string()
255-
.optional()
256-
.describe(
257-
"Raw JSON input — bypasses all other flags (use '-' for stdin)"
258-
),
259-
};
260-
261254
let p = parser().name(commandName).options(options);
262255

263256
const positionalFields = fields.filter((f) => f.isPositional);
@@ -272,10 +265,27 @@ export function buildParser(
272265
p = p.args(args);
273266
}
274267

268+
let parseResult: Record<string, unknown> | undefined;
269+
270+
p._internalHandler((r) => {
271+
switch (r.type) {
272+
case "match":
273+
parseResult = r.parsed as Record<string, unknown>;
274+
break;
275+
case "error":
276+
throw new ValidationError(r.error.message);
277+
case "help":
278+
case "version":
279+
break;
280+
}
281+
});
282+
275283
return {
276-
parse: (argv: string[]) => p.parse(argv) as Record<string, unknown>,
277-
showHelp: () => {
278-
p.parse(["--help"]);
284+
parse: (argv: string[]): Record<string, unknown> => {
285+
parseResult = undefined;
286+
p.parse(argv);
287+
if (!parseResult) throw new ValidationError("Failed to parse arguments");
288+
return parseResult;
279289
},
280290
jsonFallbackFields,
281291
};
@@ -337,9 +347,8 @@ export async function parseCommand(
337347
const built = buildParser(schema, commandName, positionalArgs, cliTransforms);
338348
const result = built.parse(argv);
339349

340-
const { json: _json, ...rawParams } = result;
341350
const params: Record<string, unknown> = {};
342-
const mutableRawParams = { ...rawParams };
351+
const mutableRawParams = { ...result };
343352

344353
for (const [fieldName, transform] of Object.entries(cliTransforms ?? {})) {
345354
const values: Record<string, unknown> = {};

0 commit comments

Comments
 (0)