mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	fix: cli framework help option not work
This commit is contained in:
		@@ -94,30 +94,36 @@ function processAndExecute<TContext extends object>(
 | 
			
		||||
): Result<void, CliError> {
 | 
			
		||||
  const { command, commandPath, options, remaining } = parseResult;
 | 
			
		||||
 | 
			
		||||
  // Handle requests for help on a specific command.
 | 
			
		||||
  if (shouldShowHelp([...remaining, ...Object.keys(options)])) {
 | 
			
		||||
    writer(generateHelp(command, commandPath));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If a command has subcommands but no action, show its help page.
 | 
			
		||||
  if (
 | 
			
		||||
    command.subcommands &&
 | 
			
		||||
  // Unified Help Check:
 | 
			
		||||
  // A command should show its help page if:
 | 
			
		||||
  // 1. A help flag is explicitly passed (`--help` or `-h`). This has the highest priority.
 | 
			
		||||
  // 2. It's a command group that was called without a subcommand (i.e., it has no action).
 | 
			
		||||
  const isHelpFlagPassed = shouldShowHelp([
 | 
			
		||||
    ...remaining,
 | 
			
		||||
    ...Object.keys(options),
 | 
			
		||||
  ]);
 | 
			
		||||
  const isCommandGroupWithoutAction =
 | 
			
		||||
    command.subcommands !== undefined &&
 | 
			
		||||
    command.subcommands.size > 0 &&
 | 
			
		||||
    command.action === undefined
 | 
			
		||||
  ) {
 | 
			
		||||
    command.action === undefined;
 | 
			
		||||
 | 
			
		||||
  if (isHelpFlagPassed || isCommandGroupWithoutAction) {
 | 
			
		||||
    writer(generateHelp(command, commandPath));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // A command that is meant to be executed must have an action.
 | 
			
		||||
  // If we are here, it's a runnable command. It must have an action.
 | 
			
		||||
  if (command.action === undefined) {
 | 
			
		||||
    // This case should ideally not be reached if the parser and the logic above are correct.
 | 
			
		||||
    // It would mean a command has no action and no subcommands, which is a configuration error.
 | 
			
		||||
    return new Err({
 | 
			
		||||
      kind: "NoAction",
 | 
			
		||||
      commandPath: [...commandPath, command.name],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Now we know it's a runnable command, and no help flag was passed.
 | 
			
		||||
  // We can now safely process the remaining items as arguments.
 | 
			
		||||
  return processArguments(command.args ?? [], remaining)
 | 
			
		||||
    .andThen((args) => {
 | 
			
		||||
      return processOptions(
 | 
			
		||||
 
 | 
			
		||||
@@ -103,5 +103,5 @@ export function generateCommandList<TContext extends object>(
 | 
			
		||||
 * @returns `true` if a help flag is found, otherwise `false`.
 | 
			
		||||
 */
 | 
			
		||||
export function shouldShowHelp(argv: string[]): boolean {
 | 
			
		||||
  return argv.includes("--help") || argv.includes("-h");
 | 
			
		||||
  return argv.includes("help") || argv.includes("h");
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,22 +87,49 @@ export function parseArguments<TContext extends object>(
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let currentCommand = rootCommand;
 | 
			
		||||
  let i = 0;
 | 
			
		||||
  let inOptions = false;
 | 
			
		||||
 | 
			
		||||
  const optionMapCache = new OptionMapCache();
 | 
			
		||||
  const getCurrentOptionMaps = () =>
 | 
			
		||||
    getOptionMaps(optionMapCache, currentCommand);
 | 
			
		||||
 | 
			
		||||
  while (i < argv.length) {
 | 
			
		||||
  // Cache option maps for current command - only updated when command changes
 | 
			
		||||
  let currentOptionMaps = getOptionMaps(optionMapCache, currentCommand);
 | 
			
		||||
 | 
			
		||||
  // Helper function to update command context and refresh option maps
 | 
			
		||||
  const updateCommand = (
 | 
			
		||||
    newCommand: Command<TContext>,
 | 
			
		||||
    commandName: string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    currentCommand = newCommand;
 | 
			
		||||
    result.command = currentCommand;
 | 
			
		||||
    result.commandPath.push(commandName);
 | 
			
		||||
    currentOptionMaps = getOptionMaps(optionMapCache, currentCommand);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Helper function to process option value
 | 
			
		||||
  const processOption = (optionName: string, i: number): number => {
 | 
			
		||||
    const optionDef = currentOptionMaps.optionMap.get(optionName);
 | 
			
		||||
    const nextArg = argv[i + 1];
 | 
			
		||||
    const isKnownBooleanOption =
 | 
			
		||||
      optionDef !== undefined && optionDef.defaultValue === undefined;
 | 
			
		||||
    const nextArgLooksLikeValue =
 | 
			
		||||
      nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
 | 
			
		||||
 | 
			
		||||
    if (nextArgLooksLikeValue && !isKnownBooleanOption) {
 | 
			
		||||
      result.options[optionName] = nextArg;
 | 
			
		||||
      return i + 1; // Skip the value argument
 | 
			
		||||
    } else {
 | 
			
		||||
      result.options[optionName] = true;
 | 
			
		||||
      return i;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Single pass through argv
 | 
			
		||||
  for (let i = 0; i < argv.length; i++) {
 | 
			
		||||
    const arg = argv[i];
 | 
			
		||||
 | 
			
		||||
    if (arg === undefined || arg === null) {
 | 
			
		||||
      i++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    // Skip null/undefined arguments
 | 
			
		||||
    if (!arg) continue;
 | 
			
		||||
 | 
			
		||||
    // Handle double dash (--) - everything after is treated as a remaining argument
 | 
			
		||||
    // Handle double dash (--) - everything after is treated as remaining
 | 
			
		||||
    if (arg === "--") {
 | 
			
		||||
      result.remaining.push(...argv.slice(i + 1));
 | 
			
		||||
      break;
 | 
			
		||||
@@ -121,70 +148,33 @@ export function parseArguments<TContext extends object>(
 | 
			
		||||
      } else {
 | 
			
		||||
        // --option [value] format
 | 
			
		||||
        const optionName = arg.slice(2);
 | 
			
		||||
        const optionDef = getCurrentOptionMaps().optionMap.get(optionName);
 | 
			
		||||
 | 
			
		||||
        // Check if this is a known boolean option or if next arg looks like a value
 | 
			
		||||
        const nextArg = argv[i + 1];
 | 
			
		||||
        const isKnownBooleanOption =
 | 
			
		||||
          optionDef !== undefined && optionDef.defaultValue === undefined;
 | 
			
		||||
        const nextArgLooksLikeValue =
 | 
			
		||||
          nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
 | 
			
		||||
 | 
			
		||||
        if (nextArgLooksLikeValue && !isKnownBooleanOption) {
 | 
			
		||||
          result.options[optionName] = nextArg;
 | 
			
		||||
          i++; // Skip the value argument
 | 
			
		||||
        } else {
 | 
			
		||||
          // Boolean flag or no value available
 | 
			
		||||
          result.options[optionName] = true;
 | 
			
		||||
        }
 | 
			
		||||
        i = processOption(optionName, i);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle short options (-o or -o value)
 | 
			
		||||
    else if (arg.startsWith("-") && arg.length > 1) {
 | 
			
		||||
      inOptions = true;
 | 
			
		||||
      const shortName = arg.slice(1);
 | 
			
		||||
 | 
			
		||||
      // Get option maps for the new command (lazy loaded and cached)
 | 
			
		||||
      const maps = getCurrentOptionMaps();
 | 
			
		||||
      const optionName = maps.shortNameMap.get(shortName) ?? shortName;
 | 
			
		||||
      const optionDef = maps.optionMap.get(optionName);
 | 
			
		||||
 | 
			
		||||
      // Check if this is a known boolean option or if next arg looks like a value
 | 
			
		||||
      const nextArg = argv[i + 1];
 | 
			
		||||
      const isKnownBooleanOption =
 | 
			
		||||
        optionDef !== undefined && optionDef.defaultValue === undefined;
 | 
			
		||||
      const nextArgLooksLikeValue =
 | 
			
		||||
        nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
 | 
			
		||||
 | 
			
		||||
      if (nextArgLooksLikeValue && !isKnownBooleanOption) {
 | 
			
		||||
        result.options[optionName] = nextArg;
 | 
			
		||||
        i++; // Skip the value argument
 | 
			
		||||
      } else {
 | 
			
		||||
        // Boolean flag or no value available
 | 
			
		||||
        result.options[optionName] = true;
 | 
			
		||||
      }
 | 
			
		||||
      const optionName =
 | 
			
		||||
        currentOptionMaps.shortNameMap.get(shortName) ?? shortName;
 | 
			
		||||
      i = processOption(optionName, i);
 | 
			
		||||
    }
 | 
			
		||||
    // Handle positional arguments and command resolution
 | 
			
		||||
    else {
 | 
			
		||||
      if (!inOptions) {
 | 
			
		||||
        // Try to find this as a subcommand of the current command
 | 
			
		||||
        const subcommand = currentCommand.subcommands?.get(arg);
 | 
			
		||||
        if (subcommand !== undefined) {
 | 
			
		||||
          // Found a subcommand, move deeper
 | 
			
		||||
          currentCommand = subcommand;
 | 
			
		||||
          result.command = currentCommand;
 | 
			
		||||
          result.commandPath.push(arg);
 | 
			
		||||
        if (subcommand) {
 | 
			
		||||
          updateCommand(subcommand, arg);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Not a subcommand, treat as remaining argument
 | 
			
		||||
          result.remaining.push(arg);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // After options have started, treat as a remaining argument
 | 
			
		||||
        // After options have started, treat as remaining argument
 | 
			
		||||
        result.remaining.push(arg);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Ok(result);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user