javascriptnode.jsnode-commander

CommanderJS - Prompt for missing args/options


I'm trying to implement prompting users for missing required args/mandatory options.

I thought I could use a preSubcommand hook, change process.argv with prompts, and then allow commander to parse the updated argv.

My hook gets executed before any exit however, After the complete of my hook's callback, I will get this error: error: required option '--foo' not specified.

It seems that parsing continues after the hook on the old process.argv.

For example my-cli subcommand --foo bar will work fine. my-cli subcommand will prompt for foo and push it to process.argv inside the hook but then exit with the error above.

Any ideas? I tried to suppress this error with exitOverride and re-calling parse afterwards, but my callback is not being reached. Surrounding in try-catch doesn't work either.

Here's my code:

program
        .name('cli')
        .description('foo')
        .version(version, '-v, --version')
        .addCommand(sub)
        .hook('preSubcommand', async (program, subcommand) => {
            await promptForMissingArgs(program, subcommand);
            console.log('here');
            console.log(process.argv);
            program.parse(process.argv); // called again with correct process.argv but still exits
        })
        .exitOverride((exitCode) => {
            console.log(`Exiting with code ${exitCode.exitCode}`); // not reached
            //  if (exitCode.exitCode !== 0) {
            //     program.parse(process.argv);
            //  }
        });
    // parse command line arguments and execute command
    try {
        program.parse(process.argv);
    } catch (error) {
        console.log('here'); // doesn't reach here
        console.error(error);
    }

From the docs I see this: if the first arg is a subcommand call preSubcommand hooks pass remaining arguments to subcommand, and process same way https://github.com/tj/commander.js/blob/HEAD/docs/parsing-and-hooks.md

Is it because my hook is async?


Solution

  • Instead of trying to change process.argv, I was able to set options during preSubcommand hook. I was not able to find a way to change argument values so I settled with options.

    @shadowspawn answer is also valid but I have multiple subcommands so I chose to keep my prompting in the shared preSubcommand hook rather than preAction. This also let me keep options as mandatory because they are not checked yet at this stage in the parsing lifecycle

    import { Command } from 'commander';
    
    const program = new Command();
    const subcommand= new Command();
    
    (async () => {
    subcommand
      .option('-p, --port <number>')
      .action(async (options) => {
        console.log(options);
      });
    
    program.addCommand(subcommand)
      .hook('preSubcommand', async (thisCommand, sub) => {
        await promptForMissingArgs(thisCommand, sub);
      })
    
    await program.parseAsync();
    })();
    

    With promptForMissingArgs implementation something like this:

    async function promptForMissingArgs(command, subcommand) {
      const answers = await inquirer.prompt(getMissingFromArgv(subcommand.opts()));
      Object.keys(answers).forEach(ans => subcommand.setOptionValue(ans, answers[ans]));
    }
    
    function getMissingFromArgv(opts) {
      // compare process.argv against mandatory opts() to see what is missing
      // return list of prompts for inquirer
    }