From 6c2ee936a1e9912d9c15d0123d098e99934fe447 Mon Sep 17 00:00:00 2001 From: Tom Leverstone Date: Wed, 8 Jul 2026 09:20:55 +0100 Subject: [PATCH] Standardise installation instructions --- README.md | 38 ++++++++++---------- grantguard.py | 84 ++------------------------------------------ grantguard/_entry.py | 83 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 +-- 4 files changed, 106 insertions(+), 104 deletions(-) create mode 100644 grantguard/_entry.py diff --git a/README.md b/README.md index a3ff157..b3fb02e 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ or CDN-hosted scripts. ### Requirements -- [`uv`](https://docs.astral.sh/uv) - a Python package and project manager, a modern best-in-class standard for python projects. GrantGuard uses `uv run` as its supported launch path, allowing consistent and convenient execution across MacOS, Linux, and Windows. -- `git` - for cloning this repo +- [`uv`](https://docs.astral.sh/uv) - a Python package and project manager, a modern best-in-class standard for python projects. - A supported browser for the Web UI GrantGuard uses browser-native HTML/CSS/JS and is intended for current stable @@ -52,15 +51,14 @@ versions of Chrome, Edge, Firefox, and Safari. 0. If you haven't already, [install `uv`](https://github.com/astral-sh/uv#installation). -1. Clone this repo +1. Run GrantGuard directly ```bash - git clone https://github.com/OpenVanta/grantguard.git + uvx --from git+https://github.com/OpenVanta/GrantGuard grantguard ``` -2. Run the web UI +That opens the web UI. You can also run the CLI audit the same way: ```bash - cd grantguard - uv run grantguard.py + uvx --from git+https://github.com/OpenVanta/GrantGuard grantguard audit ``` By default, GrantGuard reviews user-level Claude settings sources only. It does @@ -72,8 +70,8 @@ project directories unless you ask it to. ### Synopsis ```bash -uv run grantguard.py ui [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--port PORT] [--no-open] -uv run grantguard.py audit [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--show-safe] [--fix] +grantguard ui [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--port PORT] [--no-open] +grantguard audit [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--show-safe] [--fix] ``` ### Defaults @@ -89,7 +87,7 @@ An empty selection is a successful empty audit. GrantGuard prints that no Claude settings sources were found and exits `0`. ```bash -uv run grantguard.py audit +grantguard audit ``` ### Targets And Scans @@ -98,31 +96,31 @@ Targets may be passed positionally or with repeatable `--targets PATH`; both forms behave identically. ```bash -uv run grantguard.py audit /path/to/repo -uv run grantguard.py audit --targets /path/to/repo -uv run grantguard.py audit --targets /repo/a --targets /repo/b +grantguard audit /path/to/repo +grantguard audit --targets /path/to/repo +grantguard audit --targets /repo/a --targets /repo/b ``` Use `--scan` to shallowly discover `.claude/settings*.json` below one or more target roots: ```bash -uv run grantguard.py audit --scan --targets /path/to/workspace +grantguard audit --scan --targets /path/to/workspace ``` Use `--deep-scan` for deeper discovery under target roots, or without targets for broad discovery: ```bash -uv run grantguard.py audit --deep-scan --targets /path/to/workspace -uv run grantguard.py audit --deep-scan +grantguard audit --deep-scan --targets /path/to/workspace +grantguard audit --deep-scan ``` ### Tolerance ```bash -uv run grantguard.py audit --tolerance default -uv run grantguard.py audit --tolerance permissive +grantguard audit --tolerance default +grantguard audit --tolerance permissive ``` `default` flags high-risk findings and overbroad wildcard rules. `permissive` @@ -133,8 +131,8 @@ keeps overbroad wildcard rules and flags only higher-risk findings. `audit` is read-only unless `--fix` is present. ```bash -uv run grantguard.py audit --fix -uv run grantguard.py audit --tolerance permissive --fix +grantguard audit --fix +grantguard audit --tolerance permissive --fix ``` `audit --fix` writes to editable Claude settings files in scope and removes all diff --git a/grantguard.py b/grantguard.py index d20dfe8..d1c88e3 100755 --- a/grantguard.py +++ b/grantguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -GrantGuard main entrypoint. +GrantGuard entry point for ``uv run grantguard.py``. Web UI usage: uv run grantguard.py ui [TARGET ...] [--targets PATH] [--scan | --deep-scan] @@ -9,92 +9,12 @@ uv run grantguard.py audit [TARGET ...] [--targets PATH] [--scan | --deep-scan] uv run grantguard.py audit --fix # write removals to editable settings files """ -import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -def handle_serve_ui(args: argparse.Namespace) -> None: - """Serves the GrantGuard web UI (the `ui` subcommand).""" - from grantguard.server import serve - - paths = list(args.paths) + list(args.targets or []) - if args.scan and not paths: - print("grantguard ui: error: --scan requires at least one TARGET or --targets PATH", - file=sys.stderr) - sys.exit(2) - - serve( - paths=paths or None, - scan=args.scan, - deep_scan=args.deep_scan, - tolerance=args.tolerance, - port=args.port, - open_browser=not args.no_open, - ) - - -def handle_cli_audit(args: argparse.Namespace) -> None: - """Invokes the GrantGuard CLI audit (the `audit` subcommand).""" - from grantguard.cli import run_args - - sys.exit(run_args(args)) - - -def create_arg_parser() -> argparse.ArgumentParser: - """Build the top-level parser with `ui`/`audit` subcommands. - - Using subparsers (instead of slicing sys.argv by hand) keeps each command's - flags self-describing and lets the parser be exercised in isolation. - """ - from grantguard import cli - - parser = argparse.ArgumentParser( - prog="grantguard", - description="🛡️ GrantGuard — audit & clean your Claude Code permission allowlist", - ) - sub = parser.add_subparsers(dest="command") - - ui = sub.add_parser("ui", help="open the local web UI (the default command)") - ui.add_argument("paths", nargs="*", metavar="TARGET", default=[], - help="settings files or repo/.claude directories to audit") - ui.add_argument("--targets", action="append", default=[], metavar="PATH", - help="add a target path (repeatable)") - ui_scan = ui.add_mutually_exclusive_group() - ui_scan.add_argument("--scan", action="store_true", - help="shallowly discover .claude/settings*.json under target roots") - ui_scan.add_argument("--deep-scan", action="store_true", - help="deeply discover .claude/settings*.json under target roots, " - "or broadly when no targets are provided") - ui.add_argument("--tolerance", choices=("default", "permissive"), default="default") - ui.add_argument("--port", type=int, default=8770) - ui.add_argument("--no-open", action="store_true") - ui.set_defaults(func=handle_serve_ui) - - # `audit` reuses cli's flag definitions so the two parsers can't drift apart. - audit = sub.add_parser( - "audit", - help="audit & clean your allowlist (dry run by default)", - description="🛡️ GrantGuard — audit & clean your Claude Code permission allowlist", - ) - cli.add_audit_args(audit) - audit.set_defaults(func=handle_cli_audit) - - return parser - - -def main(argv=None) -> None: - argv = list(sys.argv[1:] if argv is None else argv) - # Default to the `ui` subcommand when none is given — a bare `grantguard` or - # an options-first invocation like `grantguard --port 8770` still opens the UI. - if not argv or (argv[0].startswith("-") and argv[0] not in ("-h", "--help")): - argv = ["ui", *argv] - - args = create_arg_parser().parse_args(argv) - args.func(args) - +from grantguard._entry import main # noqa: E402 if __name__ == "__main__": main() diff --git a/grantguard/_entry.py b/grantguard/_entry.py new file mode 100644 index 0000000..4df1bf9 --- /dev/null +++ b/grantguard/_entry.py @@ -0,0 +1,83 @@ +"""GrantGuard package entry point. + +Provides the ``main`` function used by the ``grantguard`` console script and by +the top-level ``grantguard.py`` shim for ``uv run grantguard.py``. +""" +import argparse +import sys + + +def handle_serve_ui(args: argparse.Namespace) -> None: + """Serves the GrantGuard web UI (the `ui` subcommand).""" + from grantguard.server import serve + + paths = list(args.paths) + list(args.targets or []) + if args.scan and not paths: + print("grantguard ui: error: --scan requires at least one TARGET or --targets PATH", + file=sys.stderr) + sys.exit(2) + + serve( + paths=paths or None, + scan=args.scan, + deep_scan=args.deep_scan, + tolerance=args.tolerance, + port=args.port, + open_browser=not args.no_open, + ) + + +def handle_cli_audit(args: argparse.Namespace) -> None: + """Invokes the GrantGuard CLI audit (the `audit` subcommand).""" + from grantguard.cli import run_args + + sys.exit(run_args(args)) + + +def create_arg_parser() -> argparse.ArgumentParser: + """Build the top-level parser with `ui`/`audit` subcommands.""" + from grantguard import cli + + parser = argparse.ArgumentParser( + prog="grantguard", + description="GrantGuard — audit & clean your Claude Code permission allowlist", + ) + sub = parser.add_subparsers(dest="command") + + ui = sub.add_parser("ui", help="open the local web UI (the default command)") + ui.add_argument("paths", nargs="*", metavar="TARGET", default=[], + help="settings files or repo/.claude directories to audit") + ui.add_argument("--targets", action="append", default=[], metavar="PATH", + help="add a target path (repeatable)") + ui_scan = ui.add_mutually_exclusive_group() + ui_scan.add_argument("--scan", action="store_true", + help="shallowly discover .claude/settings*.json under target roots") + ui_scan.add_argument("--deep-scan", action="store_true", + help="deeply discover .claude/settings*.json under target roots, " + "or broadly when no targets are provided") + ui.add_argument("--tolerance", choices=("default", "permissive"), default="default") + ui.add_argument("--port", type=int, default=8770) + ui.add_argument("--no-open", action="store_true") + ui.set_defaults(func=handle_serve_ui) + + # `audit` reuses cli's flag definitions so the two parsers can't drift apart. + audit = sub.add_parser( + "audit", + help="audit & clean your allowlist (dry run by default)", + description="GrantGuard — audit & clean your Claude Code permission allowlist", + ) + cli.add_audit_args(audit) + audit.set_defaults(func=handle_cli_audit) + + return parser + + +def main(argv=None) -> None: + argv = list(sys.argv[1:] if argv is None else argv) + # Default to the `ui` subcommand when none is given — a bare `grantguard` or + # an options-first invocation like `grantguard --port 8770` still opens the UI. + if not argv or (argv[0].startswith("-") and argv[0] not in ("-h", "--help")): + argv = ["ui", *argv] + + args = create_arg_parser().parse_args(argv) + args.func(args) diff --git a/pyproject.toml b/pyproject.toml index 0e0e6b7..7fd607f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,5 +7,6 @@ requires-python = ">=3.10" license = { text = "MIT" } dependencies = [] -[tool.uv] -package = false +[project.scripts] +grantguard = "grantguard._entry:main" +