fix(cli): derive top-level value flags from parser

(cherry picked from commit f2e5a1388115615fafde49a5f2144e5855a81d89)
This commit is contained in:
fangliquanflq
2026-08-24 12:30:13 +08:00
committed by Teknium
parent 3963fc6f21
commit 694550e486
3 changed files with 45 additions and 39 deletions
+15
View File
@@ -11,6 +11,7 @@ because its dispatch is tightly coupled to module-level ``cmd_*`` functions.
"""
import argparse
from functools import lru_cache
# `--profile` / `-p` is consumed by ``main._apply_profile_override`` before
@@ -23,6 +24,20 @@ PRE_ARGPARSE_INHERITED_FLAGS: list[tuple[str, bool]] = [
]
@lru_cache(maxsize=1)
def top_level_value_flag_sets() -> tuple[frozenset[str], frozenset[str]]:
"""Return required- and optional-value flags from the live parser."""
parser = build_top_level_parser()[0]
required: set[str] = set()
optional: set[str] = set()
for action in parser._actions:
if not action.option_strings or action.nargs == 0:
continue
target = optional if action.nargs == "?" else required
target.update(action.option_strings)
return frozenset(required), frozenset(optional)
def _inherited_flag(parser, *args, **kwargs):
"""Register a flag that ``hermes_cli.relaunch`` should carry over when
the CLI re-execs itself (e.g. after ``sessions browse`` picks a session,
+8 -39
View File
@@ -572,17 +572,9 @@ def _apply_profile_override() -> None:
# 1. Check for explicit -p / --profile flag. Historically this worked even
# after the subcommand (`hermes chat -p coder`), so keep scanning broadly.
# The exception is command-argv passthrough regions such as `mcp add --args`.
value_flags = {
"-z", "--oneshot",
"-m", "--model",
"--provider",
"-t", "--toolsets",
"-r", "--resume",
"-s", "--skills",
"--usage-file",
"--in",
}
optional_value_flags = {"-c", "--continue"}
from hermes_cli._parser import top_level_value_flag_sets
value_flags, optional_value_flags = top_level_value_flag_sets()
i = 0
while i < len(argv):
arg = argv[i]
@@ -11908,33 +11900,6 @@ _BUILTIN_SUBCOMMANDS = frozenset(
)
# Top-level flags that take a value. Needed by ``_first_positional_argv``
# so that in ``hermes -m gpt5 chat``, ``gpt5`` is correctly skipped as a
# flag value rather than misclassified as a subcommand. Kept in sync with
# the top-level flags declared in ``hermes_cli/_parser.py``.
#
# Correctness-safe either way: missing an entry here only makes the
# fast-path bail out too eagerly (we run plugin discovery when we didn't
# need to); extra entries would make us skip a real positional.
_TOP_LEVEL_VALUE_FLAGS = frozenset(
{
"-z", "--oneshot",
"-m", "--model",
"--provider",
"-t", "--toolsets",
"-r", "--resume",
"-s", "--skills",
"--usage-file",
"--in",
# ``-c / --continue`` is nargs='?' (optional value). Treat it as
# value-taking: if the next token is a subcommand-looking word
# the user almost certainly meant it as the session name, and
# either interpretation keeps us on the safe side.
"-c", "--continue",
}
)
def _first_positional_argv() -> str | None:
"""Return the first non-flag, non-flag-value token in ``sys.argv[1:]``.
@@ -11947,6 +11912,10 @@ def _first_positional_argv() -> str | None:
bar`` flags degrade gracefully (``bar`` may be wrongly classified as
a positional, which at worst forces a one-time plugin discovery).
"""
from hermes_cli._parser import top_level_value_flag_sets
required_value_flags, optional_value_flags = top_level_value_flag_sets()
value_flags = required_value_flags | optional_value_flags
argv = sys.argv[1:]
i = 0
while i < len(argv):
@@ -11961,7 +11930,7 @@ def _first_positional_argv() -> str | None:
if "=" in tok:
i += 1
continue
if tok in _TOP_LEVEL_VALUE_FLAGS and i + 1 < len(argv):
if tok in value_flags and i + 1 < len(argv):
i += 2
continue
i += 1
@@ -28,6 +28,7 @@ from unittest.mock import patch
import pytest
from hermes_cli._parser import build_top_level_parser, top_level_value_flag_sets
from hermes_cli.main import (
_BUILTIN_SUBCOMMANDS,
_first_positional_argv,
@@ -69,6 +70,27 @@ def _live_subcommand_names() -> set[str]:
# ── _first_positional_argv ─────────────────────────────────────────────────
def test_value_flag_sets_match_top_level_parser():
required, optional = top_level_value_flag_sets()
for action in build_top_level_parser()[0]._actions:
if not action.option_strings or action.nargs == 0:
continue
expected = optional if action.nargs == "?" else required
assert set(action.option_strings) <= expected
def test_reasoning_value_is_not_misclassified_as_subcommand(monkeypatch):
monkeypatch.setattr(
sys,
"argv",
["hermes", "--reasoning", "high", "chat", "hello"],
)
assert _first_positional_argv() == "chat"
assert _plugin_cli_discovery_needed() is False
# ── _plugin_cli_discovery_needed ───────────────────────────────────────────