diff --git a/.changeset/dark-rocks-smoke.md b/.changeset/dark-rocks-smoke.md new file mode 100644 index 000000000000..c181184d45ad --- /dev/null +++ b/.changeset/dark-rocks-smoke.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fixed Wrangler's error handling for both invalid commands with and without the `--help` flag, ensuring consistent and clear error messages. + +Additionally, it also ensures that if you provide an invalid argument to a valid command, Wrangler will now correctly display that specific commands help menu. diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index 08851b3b1a47..52e1c5954aa4 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -168,6 +168,36 @@ describe("wrangler", () => { " `); }); + + it("should display an error even with --help flag", async () => { + await expect( + runWrangler("invalid-command --help") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unknown argument: invalid-command]` + ); + + expect(std.err).toContain("Unknown argument: invalid-command"); + expect(std.out).toContain("wrangler"); + expect(std.out).toContain("COMMANDS"); + expect(std.out).toContain("ACCOUNT"); + }); + }); + + describe("invalid flag on valid command", () => { + it("should display command-specific help for unknown flag", async () => { + await expect( + runWrangler("types --invalid-flag-xyz") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unknown arguments: invalid-flag-xyz, invalidFlagXyz]` + ); + + expect(std.err).toContain("Unknown arguments: invalid-flag-xyz"); + expect(std.out).toContain("wrangler types"); + expect(std.out).toContain( + "Generate types from your Worker configuration" + ); + expect(std.out).not.toContain("ACCOUNT"); + }); }); describe("global options", () => { diff --git a/packages/wrangler/src/core/CommandRegistry.ts b/packages/wrangler/src/core/CommandRegistry.ts index ad9c22f12c62..d0286232a780 100644 --- a/packages/wrangler/src/core/CommandRegistry.ts +++ b/packages/wrangler/src/core/CommandRegistry.ts @@ -67,6 +67,12 @@ export class CommandRegistry { */ #categories: CategoryMap; + /** + * Set of legacy command names registered outside the `CommandRegistry` class. + * Used to track commands like `containers`, `pubsub`, etc. + */ + #legacyCommands: Set; + /** * Initializes the command registry with the given command registration function. */ @@ -76,6 +82,7 @@ export class CommandRegistry { this.#registerCommand = registerCommand; this.#tree = this.#DefinitionTreeRoot.subtree; this.#categories = new Map(); + this.#legacyCommands = new Set(); } /** @@ -134,6 +141,19 @@ export class CommandRegistry { this.#walkTreeAndRegister(namespace, node, `wrangler ${namespace}`); } + /** + * Get a set of all top-level command names. + * + * Includes both registry-defined commands & legacy commands. + */ + get topLevelCommands(): Set { + const commands = new Set(this.#tree.keys()); + for (const legacyCmd of this.#legacyCommands) { + commands.add(legacyCmd); + } + return commands; + } + /** * Returns the map of categories to command segments, ordered according to * the category order. Commands within each category are sorted alphabetically. @@ -163,6 +183,14 @@ export class CommandRegistry { return orderedCategories; } + /** + * Registers a legacy command that doesn't use the `CommandRegistry` class. + * This is used for hidden commands like `cloudchamber` that use the old yargs pattern. + */ + registerLegacyCommand(command: string): void { + this.#legacyCommands.add(command); + } + /** * Registers a category for a legacy command that doesn't use the CommandRegistry. * This is used for commands like `containers`, `pubsub`, etc, that use the old yargs pattern. @@ -171,6 +199,9 @@ export class CommandRegistry { command: string, category: MetadataCategory ): void { + // Track as a legacy command for `topLevelCommands` getter + this.#legacyCommands.add(command); + const existing = this.#categories.get(category) ?? []; if (existing.includes(command)) { return; diff --git a/packages/wrangler/src/core/handle-errors.ts b/packages/wrangler/src/core/handle-errors.ts index 4752a0702c5e..a293cdd51ba5 100644 --- a/packages/wrangler/src/core/handle-errors.ts +++ b/packages/wrangler/src/core/handle-errors.ts @@ -426,19 +426,31 @@ export async function handleError( // The workaround is to re-run the parsing with an additional `--help` flag, which will result in the correct help message being displayed. // The `wrangler` object is "frozen"; we cannot reuse that with different args, so we must create a new CLI parser to generate the help message. - // Check if this is a root-level error (unknown argument at root level) - // by looking at the error message - if it says "Unknown argument" or "Unknown command", - // and there's only one non-flag argument, show the categorized root help const nonFlagArgs = subCommandParts.filter( (arg) => !arg.startsWith("-") && arg !== "" ); - const isRootLevelError = - nonFlagArgs.length <= 1 && - (e.message.includes("Unknown argument") || - e.message.includes("Unknown command")); + + const isUnknownArgOrCommand = + e.message.includes("Unknown argument") || + e.message.includes("Unknown command"); + + const unknownArgsMatch = e.message.match(/Unknown arguments?: (.+)/); + const unknownArgs = unknownArgsMatch + ? unknownArgsMatch[1].split(", ").map((a) => a.trim()) + : []; + + // Check if any of the unknown args match the first non-flag argument + // If so, it's an unknown command (not an unknown flag on a valid command) + // Note: we check !arg.startsWith("-") to exclude flag-like args, + // but command names can contain dashes (e.g., "dispatch-namespace") + const isUnknownCommand = unknownArgs.some( + (arg) => arg === nonFlagArgs[0] && !arg.startsWith("-") + ); + + const isRootLevelError = isUnknownArgOrCommand && isUnknownCommand; const { wrangler, showHelpWithCategories } = createCLIParser([ - ...(isRootLevelError ? [] : subCommandParts), + ...(isRootLevelError ? [] : nonFlagArgs), "--help", ]); diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 12684a58e541..8c8c2fe9ecde 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -1725,6 +1725,7 @@ export function createCLIParser(argv: string[]) { // This set to false to allow overwrite of default behaviour wrangler.version(false); + registry.registerLegacyCommand("cloudchamber"); registry.registerLegacyCommandCategory("containers", "Compute & AI"); registry.registerLegacyCommandCategory("pubsub", "Compute & AI"); @@ -1746,16 +1747,29 @@ export async function main(argv: string[]): Promise { // Check if this is a root-level help request (--help or -h with no subcommand) // In this case, we use our custom help formatter to show command categories - const isRootHelpRequest = - (argv.includes("--help") || argv.includes("-h")) && - argv.filter((arg) => !arg.startsWith("-")).length === 0; + const hasHelpFlag = argv.includes("--help") || argv.includes("-h"); + const nonFlagArgs = argv.filter((arg) => !arg.startsWith("-")); + const isRootHelpRequest = hasHelpFlag && nonFlagArgs.length === 0; - const { wrangler, showHelpWithCategories } = createCLIParser(argv); + const { wrangler, registry, showHelpWithCategories } = createCLIParser(argv); if (isRootHelpRequest) { await showHelpWithCategories(); return; } + + // Check for unknown command with a `--help` flag + const [subCommand] = nonFlagArgs; + if (hasHelpFlag && subCommand) { + const knownCommands = registry.topLevelCommands; + if (!knownCommands.has(subCommand)) { + logger.info(""); + logger.error(`Unknown argument: ${subCommand}`); + await showHelpWithCategories(); + throw new CommandLineArgsError(`Unknown argument: ${subCommand}`); + } + } + let command: string | undefined; let metricsArgs: Record | undefined; let dispatcher: ReturnType | undefined;