Default interactive CLI to menu

This commit is contained in:
pleb
2026-07-01 11:59:01 -07:00
parent 69f9dbd9b1
commit eed12376c6
4 changed files with 35 additions and 2 deletions
+2
View File
@@ -46,6 +46,8 @@ Guidance for coding agents working in this repo.
- Be careful with duplicate instance names across Windows and local roots. Use - Be careful with duplicate instance names across Windows and local roots. Use
the menu or pass `--instances-root` explicitly when targeting one install, and the menu or pass `--instances-root` explicitly when targeting one install, and
keep install/bootstrap state separate per target root. keep install/bootstrap state separate per target root.
- After completing repo changes, suggest a concise commit message in the final
response unless the user already asked you to commit.
## Validation ## Validation
+4 -1
View File
@@ -66,9 +66,12 @@ backups, and installed file records for different game trees.
For normal use, run the Textual menu from the repo root: For normal use, run the Textual menu from the repo root:
```sh ```sh
PYTHONPATH=src python -m plugin_helper menu PYTHONPATH=src python -m plugin_helper
``` ```
That is equivalent to `PYTHONPATH=src python -m plugin_helper menu` when run
from an interactive terminal.
The menu reads `plugin-helper.local.toml` when present, shows each discovered The menu reads `plugin-helper.local.toml` when present, shows each discovered
Beat Saber install with its resolved state directory, and lets you toggle Beat Saber install with its resolved state directory, and lets you toggle
managed plugins with arrow keys and Space. In the plugin table, use `d` to managed plugins with arrow keys and Space. In the plugin table, use `d` to
+8 -1
View File
@@ -70,7 +70,7 @@ def _add_common(parser: argparse.ArgumentParser, *, suppress_default: bool = Fal
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="plugin-helper") parser = argparse.ArgumentParser(prog="plugin-helper")
_add_common(parser) _add_common(parser)
subcommands = parser.add_subparsers(dest="command", required=True) subcommands = parser.add_subparsers(dest="command")
subcommands.add_parser( subcommands.add_parser(
"instances", "instances",
@@ -309,6 +309,13 @@ def run(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv) args = parser.parse_args(argv)
try: try:
if args.command is None:
if sys.stdin.isatty() and sys.stdout.isatty():
args.command = "menu"
else:
parser.print_help()
return 2
explicit_instances_root = getattr(args, "instances_root", None) is not None explicit_instances_root = getattr(args, "instances_root", None) is not None
explicit_state_dir = getattr(args, "state_dir", None) is not None explicit_state_dir = getattr(args, "state_dir", None) is not None
runtime = resolve_runtime_config( runtime = resolve_runtime_config(
+21
View File
@@ -4,6 +4,7 @@ import json
import tempfile import tempfile
import unittest import unittest
import os import os
from io import StringIO
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from zipfile import ZipFile from zipfile import ZipFile
@@ -180,6 +181,26 @@ state_dir = ".state"
self.assertEqual(runtime.state_root, root / "explicit-state") self.assertEqual(runtime.state_root, root / "explicit-state")
self.assertEqual(runtime.selected_profile.id, "linux") self.assertEqual(runtime.selected_profile.id, "linux")
def test_no_args_prints_help_when_not_interactive(self) -> None:
output = StringIO()
with patch("sys.stdin.isatty", return_value=False), patch("sys.stdout", output):
status = run([])
self.assertEqual(status, 2)
self.assertIn("usage: plugin-helper", output.getvalue())
self.assertIn("menu", output.getvalue())
def test_no_command_defaults_to_menu_when_interactive(self) -> None:
with (
patch("sys.stdin.isatty", return_value=True),
patch("sys.stdout.isatty", return_value=True),
patch("plugin_helper.cli._run_menu", return_value=0) as run_menu,
):
status = run(["--profile", "linux"])
self.assertEqual(status, 0)
run_menu.assert_called_once()
def test_run_ipa_timeout_returns_control(self) -> None: def test_run_ipa_timeout_returns_control(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
result = _run_ipa( result = _run_ipa(