diff --git a/dockervault/cli.py b/dockervault/cli.py index ef02476..c93d4e7 100644 --- a/dockervault/cli.py +++ b/dockervault/cli.py @@ -1,4 +1,5 @@ import argparse +import json import sys from pathlib import Path @@ -49,7 +50,7 @@ def entry_to_dict(entry): "exists": data.get("exists"), "reason": data.get("reason"), "compose_file": compose_file, - "source": data.get("source"), + "source": str(data.get("source")) if data.get("source") is not None else None, } @@ -84,12 +85,23 @@ def normalize_entries(entries): def build_plan(scan_root, entries, compose_count): normalized = normalize_entries(entries) + return { "root": str(scan_root), "compose_files_found": compose_count, "include": [e for e in normalized if e["bucket"] == "include"], "review": [e for e in normalized if e["bucket"] == "review"], "skip": [e for e in normalized if e["bucket"] == "skip"], + "summary": { + "include_count": sum(1 for e in normalized if e["bucket"] == "include"), + "review_count": sum(1 for e in normalized if e["bucket"] == "review"), + "skip_count": sum(1 for e in normalized if e["bucket"] == "skip"), + "missing_critical_count": sum( + 1 + for e in normalized + if e["bucket"] == "include" and not e.get("exists", True) + ), + }, } @@ -141,6 +153,25 @@ def print_warnings(plan): print(f" - {item['path']} (service={item['service']})") +def build_json_output(plan, borg_command=None): + output = { + "tool": "DockerVault", + "version": __version__, + "plan": plan, + "status": { + "ok": True, + "has_missing_critical": any( + not item.get("exists", True) for item in plan["include"] + ), + }, + } + + if borg_command is not None: + output["borg_command"] = borg_command + + return output + + def build_parser(): parser = argparse.ArgumentParser( description="DockerVault - Intelligent Docker backup discovery" @@ -169,6 +200,12 @@ def build_parser(): help="Generate borg command", ) + parser.add_argument( + "--json", + action="store_true", + help="Output machine-readable JSON", + ) + parser.add_argument( "--dry-run", action="store_true", @@ -237,10 +274,7 @@ def main(): plan = build_plan(scan_root, entries, len(compose_files)) - if not args.quiet: - print_plan(plan) - print_warnings(plan) - + borg_shell_command = None if args.repo: try: from .borg import build_borg_create_command, command_to_shell @@ -249,16 +283,33 @@ def main(): sys.exit(2) try: - include_paths = [item["path"] for item in plan["include"] if item["path"] != "?"] + include_paths = [ + item["path"] for item in plan["include"] if item["path"] != "?" + ] borg_command = build_borg_create_command(args.repo, include_paths) - - print("\nSuggested borg create command") - print("============================") - print(command_to_shell(borg_command)) + borg_shell_command = command_to_shell(borg_command) except Exception as e: print(f"ERROR: Failed to build borg command: {e}") sys.exit(2) + if args.json: + print( + json.dumps( + build_json_output(plan, borg_shell_command), + indent=2, + sort_keys=False, + ) + ) + else: + if not args.quiet: + print_plan(plan) + print_warnings(plan) + + if borg_shell_command: + print("\nSuggested borg create command") + print("============================") + print(borg_shell_command) + if args.automation: has_missing = any(not item.get("exists", True) for item in plan["include"]) if has_missing: