From 01fdfbef55655368133e1ae0ecab726e2f0dbb3e Mon Sep 17 00:00:00 2001 From: PartMe AI Date: Sat, 24 Oct 2026 19:15:28 +0800 Subject: [PATCH] feat: add gitguard install, uninstall, upgrade and completion --- bin/gitguard | 7 +++++ scripts/install_cli.py | 68 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100755 bin/gitguard create mode 100644 scripts/install_cli.py diff --git a/bin/gitguard b/bin/gitguard new file mode 100755 index 0000000..ac64e85 --- /dev/null +++ b/bin/gitguard @@ -0,0 +1,7 @@ +#!/bin/sh +ROOT=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +cmd=$1 +case "$cmd" in + install|uninstall|upgrade|completion) exec python3 "$ROOT/scripts/install_cli.py" "$@" ;; + *) exec python3 "$ROOT/scripts/gitflow.py" "$@" ;; +esac diff --git a/scripts/install_cli.py b/scripts/install_cli.py new file mode 100644 index 0000000..fd0fee0 --- /dev/null +++ b/scripts/install_cli.py @@ -0,0 +1,68 @@ +"""Install the gitguard launcher into a user bin directory.""" +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "bin" / "gitguard" +DEFAULT_BIN = Path.home() / ".local" / "bin" + + +def target(argv): + if "--bin-dir" in argv: + return Path(argv[argv.index("--bin-dir") + 1]).expanduser().resolve() / "gitguard" + return DEFAULT_BIN / "gitguard" + + +def payload(command, decision, **extra): + code = {"allow": 0, "deny": 1, "error": 2, "unverified": 3}.get(decision, 4) + return {"schemaVersion": "partme.guard/v1", "tool": "gitguard", "command": "gitguard." + command, + "decision": decision, "exitCode": code, "findings": [], "evidence": {}, **extra} + + +def main(argv): + command = argv[0] if argv else "install" + path = target(argv) + if command == "completion": + shell = argv[1] if len(argv) > 1 else "bash" + commands = "version discover init audit gate policy branch sync release recovery context hooks organization doctor check status report" + if shell not in ("bash", "zsh", "fish"): + print("usage: gitguard completion bash|zsh|fish", file=sys.stderr) + return 2 + if shell == "fish": + print("complete -c gitguard -f -a '" + commands + "'") + else: + print("complete -W '" + commands + "' gitguard") + return 0 + if command not in ("install", "uninstall", "upgrade"): + print("usage: gitguard install|uninstall|upgrade [--bin-dir DIR] [--dry-run]", file=sys.stderr) + return 2 + if command == "uninstall": + if not path.is_symlink() or path.resolve() != SOURCE.resolve(): + print(json.dumps(payload("uninstall", "deny", error="not_managed", path=str(path)), ensure_ascii=False)) + return 1 + path.unlink() + print(json.dumps(payload("uninstall", "allow", removed=str(path)), ensure_ascii=False)) + return 0 + if path.exists() and not path.is_symlink(): + print(json.dumps(payload(command, "deny", error="target_exists", path=str(path)), ensure_ascii=False)) + return 1 + if path.is_symlink() and path.resolve() != SOURCE.resolve(): + print(json.dumps(payload(command, "deny", error="target_is_other_symlink", path=str(path)), ensure_ascii=False)) + return 1 + if "--dry-run" in argv: + print(json.dumps(payload(command, "allow", dryRun=True, source=str(SOURCE), target=str(path)), ensure_ascii=False)) + return 0 + path.parent.mkdir(parents=True, exist_ok=True) + if path.is_symlink(): + path.unlink() + os.symlink(SOURCE, path) + print(json.dumps(payload(command, "allow", installed=True, source=str(SOURCE), target=str(path)), ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))