diff --git a/main.py b/main.py index 47510101..d0a66112 100755 --- a/main.py +++ b/main.py @@ -86,6 +86,42 @@ def extract_description_via_help(cli_script_path): except Exception: return "-" +def show_full_help_for_all(cli_dir, available): + """ + Print the full --help output for all discovered CLI commands. + """ + print(color_text("Infinito.Nexus CLI – Full Help Overview", Fore.CYAN + Style.BRIGHT)) + print() + + for folder, cmd in available: + # Build module path (cli..) + if folder: + rel = os.path.join(folder, f"{cmd}.py") + else: + rel = f"{cmd}.py" + + module = "cli." + rel[:-3].replace(os.sep, ".") + + header_path = f"{folder + '/' if folder else ''}{cmd}.py" + print(color_text("=" * 80, Fore.MAGENTA)) + print(color_text(f"Command: {header_path}", Fore.MAGENTA + Style.BRIGHT)) + print(color_text("-" * 80, Fore.MAGENTA)) + + try: + result = subprocess.run( + [sys.executable, "-m", module, "--help"], + capture_output=True, + text=True, + check=False + ) + if result.stdout: + print(result.stdout.rstrip()) + if result.stderr: + print(color_text(result.stderr.rstrip(), Fore.RED)) + except Exception as e: + print(color_text(f"Failed to get help for {header_path}: {e}", Fore.RED)) + + print() # extra spacer between commands def git_clean_repo(): subprocess.run(['git', 'clean', '-Xfd'], check=True) @@ -163,6 +199,7 @@ if __name__ == "__main__": sys.argv.remove('--log') git_clean = '--git-clean' in sys.argv and (sys.argv.remove('--git-clean') or True) infinite = '--infinite' in sys.argv and (sys.argv.remove('--infinite') or True) + help_all = '--help-all' in sys.argv and (sys.argv.remove('--help-all') or True) alarm_timeout = 60 if '--alarm-timeout' in sys.argv: i = sys.argv.index('--alarm-timeout') @@ -188,6 +225,12 @@ if __name__ == "__main__": available = list_cli_commands(cli_dir) args = sys.argv[1:] + # Global "show help for all commands" mode + if help_all: + show_full_help_for_all(cli_dir, available) + sys.exit(0) + + # Global help if not args or args[0] in ('-h', '--help'): print(color_text("Infinito.Nexus CLI 🦫🌐🖥️", Fore.CYAN + Style.BRIGHT)) @@ -195,7 +238,16 @@ if __name__ == "__main__": 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] [options]", + "Usage: infinito " + "[--sound] " + "[--no-signal] " + "[--log] " + "[--git-clean] " + "[--infinite] " + "[--help-all] " + "[--alarm-timeout ] " + "[-h|--help] " + " [options]", Fore.GREEN )) print() @@ -206,6 +258,7 @@ if __name__ == "__main__": 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() diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 604c2618..0ee1cad3 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -14,6 +14,85 @@ import main # assumes main.py lives at the project root class TestMainHelpers(unittest.TestCase): + + # ---------------------- + # Existing tests … + # ---------------------- + + @mock.patch.object(main, 'Style') + def test_color_text_wraps_text_with_color_and_reset(self, mock_style): + """ + color_text() should wrap text with the given color prefix and Style.RESET_ALL. + We patch Style.RESET_ALL to produce deterministic output. + """ + mock_style.RESET_ALL = '' + result = main.color_text("Hello", "") + self.assertEqual(result, "Hello") + + def test_list_cli_commands_with_nested_directories(self): + """ + list_cli_commands() should correctly identify CLI commands inside + nested directories and return folder paths using '/' separators. + """ + with tempfile.TemporaryDirectory() as tmpdir: + # File in root directory + root_cmd = os.path.join(tmpdir, "rootcmd.py") + with open(root_cmd, "w") as f: + f.write("import argparse\n") + + # File in nested directory: sub/innercmd.py + sub = os.path.join(tmpdir, "sub") + os.makedirs(sub, exist_ok=True) + nested_cmd = os.path.join(sub, "innercmd.py") + with open(nested_cmd, "w") as f: + f.write("import argparse\n") + + commands = main.list_cli_commands(tmpdir) + + self.assertIn((None, "rootcmd"), commands) + self.assertIn(("sub", "innercmd"), commands) + + @mock.patch('main.subprocess.run', side_effect=Exception("mocked error")) + def test_extract_description_via_help_returns_dash_on_exception(self, mock_run): + """ + extract_description_via_help() should return '-' if subprocess.run + raises any exception. + """ + result = main.extract_description_via_help("/fake/path/script.py") + self.assertEqual(result, "-") + + @mock.patch('main.subprocess.run') + def test_show_full_help_for_all_invokes_help_for_each_command(self, mock_run): + """ + show_full_help_for_all() should execute a help subprocess call for each + discovered CLI command. The module path must be correct. + """ + available = [ + (None, "deploy"), + ("build/defaults", "users"), + ] + + main.show_full_help_for_all("/fake/cli", available) + + expected_modules = {"cli.deploy", "cli.build.defaults.users"} + invoked_modules = set() + + for call in mock_run.call_args_list: + args, kwargs = call + cmd = args[0] + + # Validate invocation structure + self.assertGreaterEqual(len(cmd), 3) + self.assertEqual(cmd[1], "-m") # Second argument must be '-m' + invoked_modules.add(cmd[2]) # Module name + + # Validate flags + self.assertEqual(kwargs.get("capture_output"), True) + self.assertEqual(kwargs.get("text"), True) + self.assertEqual(kwargs.get("check"), False) + + self.assertEqual(expected_modules, invoked_modules) + def test_format_command_help_basic(self): name = "cmd" description = "A basic description"