mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-03 07:59:42 +00:00
Improve infinito CLI global and full help:
- Add ANSI color fallback when colorama is missing - Refactor global help into print_global_help() and reuse for --help and --help-all - Enhance show_full_help_for_all() with colorful Subcommand/File headers - Extend unit tests for global help, child sound handling, and failure loop Reference: https://chatgpt.com/share/692e88de-39f4-800f-ab7f-5ac178698831/share/this-conversation
This commit is contained in:
211
main.py
211
main.py
@@ -16,10 +16,31 @@ try:
|
|||||||
from colorama import init as colorama_init, Fore, Back, Style
|
from colorama import init as colorama_init, Fore, Back, Style
|
||||||
colorama_init(autoreset=True)
|
colorama_init(autoreset=True)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
class Dummy:
|
# Minimal ANSI fallback if colorama is not available
|
||||||
def __getattr__(self, name): return ''
|
class Style:
|
||||||
Fore = Back = Style = Dummy()
|
RESET_ALL = "\033[0m"
|
||||||
|
BRIGHT = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
|
||||||
|
class Fore:
|
||||||
|
BLACK = "\033[30m"
|
||||||
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
BLUE = "\033[34m"
|
||||||
|
MAGENTA = "\033[35m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
WHITE = "\033[37m"
|
||||||
|
|
||||||
|
class Back:
|
||||||
|
BLACK = "\033[40m"
|
||||||
|
RED = "\033[41m"
|
||||||
|
GREEN = "\033[42m"
|
||||||
|
YELLOW = "\033[43m"
|
||||||
|
BLUE = "\033[44m"
|
||||||
|
MAGENTA = "\033[45m"
|
||||||
|
CYAN = "\033[46m"
|
||||||
|
WHITE = "\033[47m"
|
||||||
|
|
||||||
def color_text(text, color):
|
def color_text(text, color):
|
||||||
return f"{color}{text}{Style.RESET_ALL}"
|
return f"{color}{text}{Style.RESET_ALL}"
|
||||||
@@ -94,20 +115,23 @@ def show_full_help_for_all(cli_dir, available):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
for folder, cmd in available:
|
for folder, cmd in available:
|
||||||
# Build module path (cli.<folder>.<cmd>)
|
# Build file path (e.g. "meta/j2/compiler.py")
|
||||||
|
file_path = f"{folder + '/' if folder else ''}{cmd}.py"
|
||||||
|
|
||||||
|
# Build subcommand (spaces instead of slashes, no .py)
|
||||||
if folder:
|
if folder:
|
||||||
rel = os.path.join(folder, f"{cmd}.py")
|
subcommand = f"{folder.replace('/', ' ')} {cmd}"
|
||||||
else:
|
else:
|
||||||
rel = f"{cmd}.py"
|
subcommand = cmd
|
||||||
|
|
||||||
module = "cli." + rel[:-3].replace(os.sep, ".")
|
# Colorful header
|
||||||
|
print(color_text("=" * 80, Fore.BLUE + Style.BRIGHT))
|
||||||
header_path = f"{folder + '/' if folder else ''}{cmd}.py"
|
print(color_text(f"Subcommand: {subcommand}", Fore.YELLOW + Style.BRIGHT))
|
||||||
print(color_text("=" * 80, Fore.MAGENTA))
|
print(color_text(f"File: {file_path}", Fore.CYAN))
|
||||||
print(color_text(f"Command: {header_path}", Fore.MAGENTA + Style.BRIGHT))
|
print(color_text("-" * 80, Fore.BLUE))
|
||||||
print(color_text("-" * 80, Fore.MAGENTA))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
module = "cli." + file_path[:-3].replace(os.sep, ".")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", module, "--help"],
|
[sys.executable, "-m", module, "--help"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -119,13 +143,92 @@ def show_full_help_for_all(cli_dir, available):
|
|||||||
if result.stderr:
|
if result.stderr:
|
||||||
print(color_text(result.stderr.rstrip(), Fore.RED))
|
print(color_text(result.stderr.rstrip(), Fore.RED))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(color_text(f"Failed to get help for {header_path}: {e}", Fore.RED))
|
print(color_text(f"Failed to get help for {file_path}: {e}", Fore.RED))
|
||||||
|
|
||||||
print() # extra spacer between commands
|
print() # extra spacer between commands
|
||||||
|
|
||||||
def git_clean_repo():
|
def git_clean_repo():
|
||||||
subprocess.run(['git', 'clean', '-Xfd'], check=True)
|
subprocess.run(['git', 'clean', '-Xfd'], check=True)
|
||||||
|
|
||||||
|
def print_global_help(available, cli_dir):
|
||||||
|
"""
|
||||||
|
Print the standard global help screen for the Infinito.Nexus CLI.
|
||||||
|
This is used by both --help and --help-all.
|
||||||
|
"""
|
||||||
|
print(color_text("Infinito.Nexus CLI 🦫🌐🖥️", Fore.CYAN + Style.BRIGHT))
|
||||||
|
print()
|
||||||
|
print(color_text("Your Gateway to Automated IT Infrastructure Setup", Style.DIM))
|
||||||
|
print()
|
||||||
|
print(color_text(
|
||||||
|
"Usage: infinito "
|
||||||
|
"[--sound] "
|
||||||
|
"[--no-signal] "
|
||||||
|
"[--log] "
|
||||||
|
"[--git-clean] "
|
||||||
|
"[--infinite] "
|
||||||
|
"[--help-all] "
|
||||||
|
"[--alarm-timeout <seconds>] "
|
||||||
|
"[-h|--help] "
|
||||||
|
"<command> [options]",
|
||||||
|
Fore.GREEN
|
||||||
|
))
|
||||||
|
print()
|
||||||
|
# Use bright style for headings
|
||||||
|
print(color_text("Options:", Style.BRIGHT))
|
||||||
|
print(color_text(" --sound Play startup melody and warning sounds", Fore.YELLOW))
|
||||||
|
print(color_text(" --no-signal Suppress success/failure signals", Fore.YELLOW))
|
||||||
|
print(color_text(" --log Log all proxied command output to logfile.log", Fore.YELLOW))
|
||||||
|
print(color_text(" --git-clean Remove all Git-ignored files before running", Fore.YELLOW))
|
||||||
|
print(color_text(" --infinite Run the proxied command in an infinite loop", Fore.YELLOW))
|
||||||
|
print(color_text(" --help-all Show full --help for all CLI commands", Fore.YELLOW))
|
||||||
|
print(color_text(" --alarm-timeout Stop warnings and exit after N seconds (default: 60)", Fore.YELLOW))
|
||||||
|
print(color_text(" -h, --help Show this help message and exit", Fore.YELLOW))
|
||||||
|
print()
|
||||||
|
print(color_text("Available commands:", Style.BRIGHT))
|
||||||
|
print()
|
||||||
|
|
||||||
|
current_folder = None
|
||||||
|
for folder, cmd in available:
|
||||||
|
if folder != current_folder:
|
||||||
|
if folder:
|
||||||
|
print(color_text(f"{folder}/", Fore.MAGENTA))
|
||||||
|
print()
|
||||||
|
current_folder = folder
|
||||||
|
desc = extract_description_via_help(
|
||||||
|
os.path.join(cli_dir, *(folder.split('/') if folder else []), f"{cmd}.py")
|
||||||
|
)
|
||||||
|
print(color_text(format_command_help(cmd, desc, indent=2), ''), "\n")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(color_text(
|
||||||
|
"🔗 You can chain subcommands by specifying nested directories,",
|
||||||
|
Fore.CYAN
|
||||||
|
))
|
||||||
|
print(color_text(
|
||||||
|
" e.g. `infinito build defaults users` →",
|
||||||
|
Fore.CYAN
|
||||||
|
))
|
||||||
|
print(color_text(
|
||||||
|
" corresponds to `cli/build/defaults/users.py`.",
|
||||||
|
Fore.CYAN
|
||||||
|
))
|
||||||
|
print()
|
||||||
|
print(color_text(
|
||||||
|
"Infinito.Nexus is a product of Kevin Veen-Birkenbach, https://cybermaster.space .\n",
|
||||||
|
Style.DIM
|
||||||
|
))
|
||||||
|
print(color_text(
|
||||||
|
"Test and use productively on https://infinito.nexus .\n",
|
||||||
|
Style.DIM
|
||||||
|
))
|
||||||
|
print(color_text(
|
||||||
|
"For commercial use, a license agreement with Kevin Veen-Birkenbach is required. \n",
|
||||||
|
Style.DIM
|
||||||
|
))
|
||||||
|
print(color_text("License: https://s.infinito.nexus/license", Style.DIM))
|
||||||
|
print()
|
||||||
|
print(color_text("🎉🌈 Happy IT Infrastructuring! 🚀🔧✨", Fore.MAGENTA + Style.BRIGHT))
|
||||||
|
print()
|
||||||
|
|
||||||
def play_start_intro():
|
def play_start_intro():
|
||||||
Sound.play_start_sound()
|
Sound.play_start_sound()
|
||||||
@@ -227,86 +330,18 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Global "show help for all commands" mode
|
# Global "show help for all commands" mode
|
||||||
if help_all:
|
if help_all:
|
||||||
|
# 1) Print the normal global help (same as --help)
|
||||||
|
print_global_help(available, cli_dir)
|
||||||
|
|
||||||
|
# 2) Then print detailed --help for all subcommands
|
||||||
|
print(color_text("Full detailed help for all subcommands:", Style.BRIGHT))
|
||||||
|
print()
|
||||||
show_full_help_for_all(cli_dir, available)
|
show_full_help_for_all(cli_dir, available)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
# Global help
|
# Global help
|
||||||
if not args or args[0] in ('-h', '--help'):
|
if not args or args[0] in ('-h', '--help'):
|
||||||
print(color_text("Infinito.Nexus CLI 🦫🌐🖥️", Fore.CYAN + Style.BRIGHT))
|
print_global_help(available, cli_dir)
|
||||||
print()
|
|
||||||
print(color_text("Your Gateway to Automated IT Infrastructure Setup", Style.DIM))
|
|
||||||
print()
|
|
||||||
print(color_text(
|
|
||||||
"Usage: infinito "
|
|
||||||
"[--sound] "
|
|
||||||
"[--no-signal] "
|
|
||||||
"[--log] "
|
|
||||||
"[--git-clean] "
|
|
||||||
"[--infinite] "
|
|
||||||
"[--help-all] "
|
|
||||||
"[--alarm-timeout <seconds>] "
|
|
||||||
"[-h|--help] "
|
|
||||||
"<command> [options]",
|
|
||||||
Fore.GREEN
|
|
||||||
))
|
|
||||||
print()
|
|
||||||
# Use bright style for headings
|
|
||||||
print(color_text("Options:", Style.BRIGHT))
|
|
||||||
print(color_text(" --sound Play startup melody and warning sounds", Fore.YELLOW))
|
|
||||||
print(color_text(" --no-signal Suppress success/failure signals", Fore.YELLOW))
|
|
||||||
print(color_text(" --log Log all proxied command output to logfile.log", Fore.YELLOW))
|
|
||||||
print(color_text(" --git-clean Remove all Git-ignored files before running", Fore.YELLOW))
|
|
||||||
print(color_text(" --infinite Run the proxied command in an infinite loop", Fore.YELLOW))
|
|
||||||
print(color_text(" --help-all Show full --help for all CLI commands", Fore.YELLOW))
|
|
||||||
print(color_text(" --alarm-timeout Stop warnings and exit after N seconds (default: 60)", Fore.YELLOW))
|
|
||||||
print(color_text(" -h, --help Show this help message and exit", Fore.YELLOW))
|
|
||||||
print()
|
|
||||||
print(color_text("Available commands:", Style.BRIGHT))
|
|
||||||
print()
|
|
||||||
|
|
||||||
current_folder = None
|
|
||||||
for folder, cmd in available:
|
|
||||||
if folder != current_folder:
|
|
||||||
if folder:
|
|
||||||
print(color_text(f"{folder}/", Fore.MAGENTA))
|
|
||||||
print()
|
|
||||||
current_folder = folder
|
|
||||||
desc = extract_description_via_help(
|
|
||||||
os.path.join(cli_dir, *(folder.split('/') if folder else []), f"{cmd}.py")
|
|
||||||
)
|
|
||||||
print(color_text(format_command_help(cmd, desc, indent=2), ''), "\n")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(color_text(
|
|
||||||
"🔗 You can chain subcommands by specifying nested directories,",
|
|
||||||
Fore.CYAN
|
|
||||||
))
|
|
||||||
print(color_text(
|
|
||||||
" e.g. `infinito build defaults users` →",
|
|
||||||
Fore.CYAN
|
|
||||||
))
|
|
||||||
print(color_text(
|
|
||||||
" corresponds to `cli/build/defaults/users.py`.",
|
|
||||||
Fore.CYAN
|
|
||||||
))
|
|
||||||
print()
|
|
||||||
print(color_text(
|
|
||||||
"Infinito.Nexus is a product of Kevin Veen-Birkenbach, https://cybermaster.space .\n",
|
|
||||||
Style.DIM
|
|
||||||
))
|
|
||||||
print(color_text(
|
|
||||||
"Test and use productively on https://infinito.nexus .\n",
|
|
||||||
Style.DIM
|
|
||||||
))
|
|
||||||
print(color_text(
|
|
||||||
"For commercial use, a license agreement with Kevin Veen-Birkenbach is required. \n",
|
|
||||||
Style.DIM
|
|
||||||
))
|
|
||||||
print(color_text("License: https://s.infinito.nexus/license", Style.DIM))
|
|
||||||
print()
|
|
||||||
print(color_text("🎉🌈 Happy IT Infrastructuring! 🚀🔧✨", Fore.MAGENTA + Style.BRIGHT))
|
|
||||||
print()
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Directory-specific help
|
# Directory-specific help
|
||||||
|
|||||||
@@ -146,6 +146,79 @@ class TestMainHelpers(unittest.TestCase):
|
|||||||
description = main.extract_description_via_help("/fake/path/empty.py")
|
description = main.extract_description_via_help("/fake/path/empty.py")
|
||||||
self.assertEqual(description, "-")
|
self.assertEqual(description, "-")
|
||||||
|
|
||||||
|
@mock.patch('main.extract_description_via_help')
|
||||||
|
@mock.patch('main.format_command_help')
|
||||||
|
@mock.patch('builtins.print')
|
||||||
|
def test_print_global_help_uses_helpers_per_command(self, mock_print, mock_fmt, mock_extract):
|
||||||
|
"""
|
||||||
|
print_global_help() should call extract_description_via_help() and
|
||||||
|
format_command_help() once per available command, with correct paths.
|
||||||
|
"""
|
||||||
|
cli_dir = "/tmp/cli"
|
||||||
|
available = [
|
||||||
|
(None, "rootcmd"),
|
||||||
|
("meta/j2", "compiler"),
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_extract.return_value = "DESC"
|
||||||
|
mock_fmt.side_effect = lambda name, desc, **kwargs: f"{name}:{desc}"
|
||||||
|
|
||||||
|
main.print_global_help(available, cli_dir)
|
||||||
|
|
||||||
|
# extract_description_via_help should be called with the correct .py paths
|
||||||
|
expected_paths = [
|
||||||
|
os.path.join(cli_dir, "rootcmd.py"),
|
||||||
|
os.path.join(cli_dir, "meta", "j2", "compiler.py"),
|
||||||
|
]
|
||||||
|
called_paths = [call.args[0] for call in mock_extract.call_args_list]
|
||||||
|
self.assertEqual(expected_paths, called_paths)
|
||||||
|
|
||||||
|
# format_command_help should be called for both commands, in order
|
||||||
|
called_names = [call.args[0] for call in mock_fmt.call_args_list]
|
||||||
|
self.assertEqual(["rootcmd", "compiler"], called_names)
|
||||||
|
|
||||||
|
@mock.patch('builtins.print')
|
||||||
|
def test__play_in_child_failure_returns_false_and_prints_warning(self, mock_print):
|
||||||
|
"""
|
||||||
|
_play_in_child() should return False and print a diagnostic
|
||||||
|
when the child exitcode is non-zero.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class FakeProcess:
|
||||||
|
def __init__(self, target=None, args=None):
|
||||||
|
self.exitcode = 1
|
||||||
|
def start(self):
|
||||||
|
pass
|
||||||
|
def join(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with mock.patch('main.Process', FakeProcess):
|
||||||
|
ok = main._play_in_child("play_warning_sound")
|
||||||
|
|
||||||
|
self.assertFalse(ok)
|
||||||
|
self.assertTrue(
|
||||||
|
any("[sound] child" in str(c.args[0]) for c in mock_print.call_args_list),
|
||||||
|
"Expected a diagnostic print when exitcode != 0",
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('main._play_in_child')
|
||||||
|
@mock.patch('main.time.sleep')
|
||||||
|
def test_failure_with_warning_loop_no_signal_skips_sounds_and_exits(
|
||||||
|
self, mock_sleep, mock_play
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
When no_signal=True, failure_with_warning_loop() should not call
|
||||||
|
_play_in_child at all and should exit after the timeout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Simulate time.monotonic jumping past the timeout immediately
|
||||||
|
with mock.patch('main.time.monotonic', side_effect=[0.0, 100.0]):
|
||||||
|
with mock.patch('main.sys.exit', side_effect=SystemExit) as mock_exit:
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
main.failure_with_warning_loop(no_signal=True, sound_enabled=True, alarm_timeout=1)
|
||||||
|
|
||||||
|
mock_play.assert_not_called()
|
||||||
|
mock_exit.assert_called() # ensure we attempted to exit
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user