reware-lighthouse 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ """Lighthouse developer CLI."""
2
+
3
+
4
+ def __getattr__(name: str) -> str:
5
+ if name == "__version__":
6
+ from lighthouse_cli.identity import version
7
+
8
+ return version()
9
+ raise AttributeError(name)
@@ -0,0 +1,4 @@
1
+ from lighthouse_cli.cli import run
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(run())
lighthouse_cli/api.py ADDED
@@ -0,0 +1,39 @@
1
+ """Typed wrappers over the Lighthouse product API endpoints the CLI uses."""
2
+
3
+ from typing import Any, TypedDict
4
+
5
+ from lighthouse_cli.config import ClientConfig
6
+ from lighthouse_cli.git_remote import normalize_repo_full_name
7
+ from lighthouse_cli.http import get_json
8
+
9
+
10
+ class Project(TypedDict, total=False):
11
+ name: str
12
+ slug: str
13
+ repo_full_name: str | None
14
+
15
+
16
+ def whoami(config: ClientConfig) -> dict[str, Any]:
17
+ """GET /v1/me — verify the API key and return {user, team}."""
18
+ body = get_json(config, "/v1/me")
19
+ return body if isinstance(body, dict) else {}
20
+
21
+
22
+ def list_projects(config: ClientConfig) -> list[Project]:
23
+ """GET /v1/projects — the team's registered projects."""
24
+ body = get_json(config, "/v1/projects")
25
+ return [p for p in body if isinstance(p, dict)] if isinstance(body, list) else []
26
+
27
+
28
+ def match_project(projects: list[Project], identifier: str) -> Project | None:
29
+ """Find the project whose repo_full_name or slug matches the identifier."""
30
+ wanted = identifier.strip().lower()
31
+ normalized = normalize_repo_full_name(identifier) or wanted
32
+ for project in projects:
33
+ repo = (project.get("repo_full_name") or "").lower()
34
+ if (repo and repo == normalized) or project.get("slug", "").lower() == wanted:
35
+ return project
36
+ return None
37
+
38
+
39
+ __all__ = ["Project", "list_projects", "match_project", "whoami"]
lighthouse_cli/cli.py ADDED
@@ -0,0 +1,100 @@
1
+ """The `reware-lighthouse` command dispatcher.
2
+
3
+ Each subcommand owns its own argument parsing, so the dispatcher just routes the
4
+ first token and forwards the rest. New subcommands register in ``_COMMANDS``.
5
+ """
6
+
7
+ import sys
8
+ from collections.abc import Callable
9
+
10
+ from lighthouse_cli import ui
11
+ from lighthouse_cli.commands import deinit, init, login, logout, scan, status
12
+ from lighthouse_cli.config import ClientConfigError
13
+ from lighthouse_cli.http import ClientRequestError
14
+ from lighthouse_cli.identity import COMMAND, NotInstalledError, version
15
+ from lighthouse_cli.link import LinkError
16
+ from lighthouse_cli.mcp import run as run_mcp
17
+
18
+ _COMMANDS: dict[str, Callable[[list[str]], int]] = {
19
+ "login": login.run,
20
+ "logout": logout.run,
21
+ "init": init.run,
22
+ "deinit": deinit.run,
23
+ "scan": scan.run,
24
+ "status": status.run,
25
+ "mcp": run_mcp,
26
+ }
27
+
28
+ _USAGE = f"""{COMMAND} — developer CLI for Lighthouse
29
+
30
+ Usage: {COMMAND} <command> [options]
31
+ {COMMAND} --version
32
+
33
+ Commands:
34
+ login Store and verify your API key
35
+ logout Remove the saved API key
36
+ init Link this repository to a project and set up tooling
37
+ deinit Remove what init set up in this repository
38
+ scan Run the pre-commit security scan
39
+ status Check your account, project and tooling (exit 0 means it works)
40
+ mcp Run the stdio MCP server (usually launched by your coding agent)
41
+ help Show help for a command: {COMMAND} help <command>
42
+
43
+ Prompt enhancement runs only when you ask for it in your coding agent:
44
+ /lighthouse-enhance <request> in Claude Code, the lighthouse-enhance skill in Codex.
45
+
46
+ Run `{COMMAND} <command> --help` for command-specific options."""
47
+
48
+
49
+ def _print_version(_argv: list[str]) -> int:
50
+ print(f"{COMMAND} {version()}")
51
+ return 0
52
+
53
+
54
+ _ORDINARY = (ClientConfigError, ClientRequestError, LinkError, OSError, ui.NoTerminalError)
55
+ _OWN_ERRORS = frozenset({"scan", "mcp"}) # scan fails open itself; mcp owns stdout
56
+
57
+
58
+ def _guarded(
59
+ handler: Callable[[list[str]], int], argv: list[str], *, ordinary: bool = False
60
+ ) -> int:
61
+ try:
62
+ return handler(argv)
63
+ except NotInstalledError as exc:
64
+ print(f"{COMMAND}: {exc}", file=sys.stderr)
65
+ return 1
66
+ except Exception as exc:
67
+ if not (ordinary and isinstance(exc, _ORDINARY)):
68
+ raise
69
+ ui.error(str(exc))
70
+ return 2 if isinstance(exc, ui.NoTerminalError) else 1
71
+ except KeyboardInterrupt:
72
+ if not ordinary:
73
+ raise
74
+ print("Interrupted.", file=sys.stderr)
75
+ return 130
76
+
77
+
78
+ def run(argv: list[str] | None = None) -> int:
79
+ for stream in (sys.stdout, sys.stderr):
80
+ # A git hook on Windows pipes output through a legacy codepage that lacks ✓ and •.
81
+ if hasattr(stream, "reconfigure"):
82
+ stream.reconfigure(errors="replace")
83
+ args = list(sys.argv[1:] if argv is None else argv)
84
+ if args[:1] == ["help"] and len(args) > 1:
85
+ args = [args[1], "--help"]
86
+ if not args or args[0] in {"-h", "--help", "help"}:
87
+ print(_USAGE)
88
+ return 0
89
+ if args[0] in {"-V", "--version"}:
90
+ return _guarded(_print_version, [])
91
+ command, rest = args[0], args[1:]
92
+ handler = _COMMANDS.get(command)
93
+ if handler is None:
94
+ print(f"{COMMAND}: unknown command '{command}'.\n\n{_USAGE}", file=sys.stderr)
95
+ return 2
96
+ return _guarded(handler, rest, ordinary=command not in _OWN_ERRORS)
97
+
98
+
99
+ if __name__ == "__main__":
100
+ raise SystemExit(run())
@@ -0,0 +1 @@
1
+ """`lighthouse` subcommands."""
@@ -0,0 +1,57 @@
1
+ """`lighthouse deinit` — undo what `init` set up in this repository.
2
+
3
+ Removes only content `init` recorded and the developer has not edited. Saved credentials
4
+ (`logout`), scan reports and the installed tool are left alone.
5
+ """
6
+
7
+ import contextlib
8
+ from pathlib import Path
9
+
10
+ from lighthouse_cli import ui
11
+ from lighthouse_cli.config import has_saved_api_key
12
+ from lighthouse_cli.git_remote import git_toplevel
13
+ from lighthouse_cli.identity import COMMAND
14
+ from lighthouse_cli.link import LINK_DIR, LinkError, remove_link
15
+ from lighthouse_cli.parsing import parser
16
+ from lighthouse_cli.setup import remove
17
+
18
+
19
+ def _remove_link(root: Path) -> bool:
20
+ try:
21
+ path = remove_link(root)
22
+ except (LinkError, OSError) as exc:
23
+ ui.warn(f"Left the project link as is: {exc}")
24
+ return False
25
+ if path:
26
+ ui.success(f"Removed {path}.")
27
+ with contextlib.suppress(OSError):
28
+ (root / LINK_DIR).rmdir() # only succeeds when nothing else lives there
29
+ return True
30
+
31
+
32
+ def run(argv: list[str] | None = None) -> int:
33
+ parser("deinit", "Remove what init set up in this repository.", "deinit").parse_args(argv)
34
+
35
+ root_path = git_toplevel()
36
+ if root_path is None:
37
+ ui.error(f"Run `{COMMAND} deinit` from inside a Git repository.")
38
+ return 1
39
+ root = Path(root_path)
40
+
41
+ report = remove(root)
42
+ for step in report.steps:
43
+ (ui.success if step.ok else ui.warn)(step.message)
44
+ linked = _remove_link(root)
45
+ if not report.complete or not linked:
46
+ ui.error("Cleanup is incomplete. Finish the items above by hand.")
47
+ return 1
48
+ if has_saved_api_key():
49
+ ui.hint(
50
+ f"Your API key stays saved (`{COMMAND} logout` removes it) and scan reports are kept."
51
+ )
52
+ else:
53
+ ui.hint("Scan reports are kept.")
54
+ return 0
55
+
56
+
57
+ __all__ = ["run"]
@@ -0,0 +1,137 @@
1
+ """`lighthouse init` — link this checkout to a dashboard project and wire up tooling.
2
+
3
+ One guided command: verify the API key, confirm the repository, match it to a
4
+ registered project, write the ``.lighthouse/project.toml`` link, then optionally
5
+ install the pre-commit hook and write the Claude/Codex MCP config and enhance skill.
6
+ """
7
+
8
+ import argparse
9
+ from pathlib import Path
10
+
11
+ from lighthouse_cli import ui
12
+ from lighthouse_cli.api import Project, list_projects, match_project
13
+ from lighthouse_cli.commands.login import ensure_login
14
+ from lighthouse_cli.config import ClientConfig, ClientConfigError
15
+ from lighthouse_cli.git_remote import detect_local_repo, git_toplevel
16
+ from lighthouse_cli.http import ClientRequestError
17
+ from lighthouse_cli.identity import COMMAND
18
+ from lighthouse_cli.link import ProjectLink, write_link
19
+ from lighthouse_cli.parsing import parser
20
+ from lighthouse_cli.setup import Integration, SetupReport, install
21
+
22
+
23
+ def _choose_identifier(args: argparse.Namespace, root: Path) -> str:
24
+ if args.project:
25
+ return args.project
26
+ detected = detect_local_repo(str(root))
27
+ if args.yes:
28
+ if not detected:
29
+ raise ClientConfigError(
30
+ "Could not detect a Git remote. Re-run with --project owner/name."
31
+ )
32
+ return detected
33
+ return ui.prompt(
34
+ "Project (owner/name)", default=detected or None, needs="--project or --yes"
35
+ ) or (detected or "")
36
+
37
+
38
+ def _verify(config: ClientConfig, identifier: str) -> Project:
39
+ projects = list_projects(config)
40
+ match = match_project(projects, identifier)
41
+ if match is not None:
42
+ return match
43
+ ui.error(f"No Lighthouse project matches '{identifier}'.")
44
+ if projects:
45
+ available = ", ".join(sorted(p.get("repo_full_name", p.get("slug", "?")) for p in projects))
46
+ ui.hint(f"Projects on your team: {available}")
47
+ ui.hint(f"Add this repository on the Lighthouse dashboard, then re-run `{COMMAND} init`.")
48
+ raise SystemExit(1)
49
+
50
+
51
+ _AGENTS = {
52
+ "claude": (Integration.CLAUDE, Integration.CLAUDE_SKILL),
53
+ "codex": (Integration.CODEX, Integration.CODEX_SKILL),
54
+ }
55
+
56
+
57
+ def _requested(args: argparse.Namespace) -> list[Integration]:
58
+ requested = []
59
+ if not args.no_hook and (
60
+ args.yes or ui.confirm("Install the pre-commit hook?", needs="--yes or --no-hook")
61
+ ):
62
+ requested.append(Integration.HOOK)
63
+ if args.no_mcp:
64
+ return requested
65
+ if args.agent:
66
+ agents = {"claude", "codex"} if args.agent == "both" else {args.agent}
67
+ elif args.yes:
68
+ agents = {"claude", "codex"}
69
+ else:
70
+ agents = set()
71
+ if ui.confirm(
72
+ "Set up Claude Code (.mcp.json and the /lighthouse-enhance skill)?",
73
+ needs="--agent, --yes or --no-mcp",
74
+ ):
75
+ agents.add("claude")
76
+ if ui.confirm(
77
+ "Set up Codex (.codex/config.toml and the lighthouse-enhance skill)?",
78
+ needs="--agent, --yes or --no-mcp",
79
+ ):
80
+ agents.add("codex")
81
+ return [*requested, *(integration for name in sorted(agents) for integration in _AGENTS[name])]
82
+
83
+
84
+ def _report(report: SetupReport) -> None:
85
+ for step in report.steps:
86
+ (ui.success if step.ok else ui.warn)(step.message)
87
+ if not report.complete:
88
+ pending = ", ".join(f"{s.integration.value} ({s.outcome.value})" for s in report.pending)
89
+ ui.error(f"Setup is incomplete. Pending: {pending}.")
90
+ ui.hint(f"Fix the items above, then re-run `{COMMAND} init`; finished steps are kept.")
91
+
92
+
93
+ def run(argv: list[str] | None = None) -> int:
94
+ cli = parser("init", "Link this repo to Lighthouse and set up tooling.", "init --yes")
95
+ cli.add_argument("--project", default=None, help="Project owner/name (skips detection).")
96
+ cli.add_argument("--yes", "-y", action="store_true", help="Accept all prompts.")
97
+ cli.add_argument("--no-hook", action="store_true", help="Skip the pre-commit hook.")
98
+ cli.add_argument("--no-mcp", action="store_true", help="Skip the agent MCP config and skill.")
99
+ cli.add_argument("--agent", choices=["claude", "codex", "both"], default=None)
100
+ args = cli.parse_args(argv)
101
+
102
+ root_path = git_toplevel()
103
+ if root_path is None:
104
+ ui.error(f"Run `{COMMAND} init` from inside a Git repository.")
105
+ return 1
106
+ root = Path(root_path)
107
+
108
+ try:
109
+ config = ensure_login(assume_yes=args.yes)
110
+ identifier = _choose_identifier(args, root)
111
+ if not identifier:
112
+ ui.error("A project identifier is required.")
113
+ return 1
114
+ project = _verify(config, identifier)
115
+ except (ClientConfigError, ClientRequestError) as exc:
116
+ ui.error(str(exc))
117
+ return 1
118
+
119
+ repo_full_name = project.get("repo_full_name") or identifier
120
+ link_path = write_link(root, ProjectLink(project=repo_full_name))
121
+ ui.success(f"Linked to '{project.get('name', repo_full_name)}' ({repo_full_name})")
122
+ ui.hint(f"Wrote {link_path}")
123
+
124
+ report = install(root, repo_full_name, _requested(args))
125
+ _report(report)
126
+ if not report.complete:
127
+ return 1
128
+
129
+ ui.heading("Done. Next steps:")
130
+ ui.hint("• Restart your coding agent, then use /lighthouse-enhance (Claude) or the")
131
+ ui.hint(" lighthouse-enhance skill (Codex) to enhance a request.")
132
+ ui.hint("• Stage a change and commit to see a scan report in .lighthouse/findings/.")
133
+ ui.hint(f"• Run `{COMMAND} status` any time to check this setup.")
134
+ return 0
135
+
136
+
137
+ __all__ = ["run"]
@@ -0,0 +1,99 @@
1
+ """`reware-lighthouse login` — store and verify the per-user API key.
2
+
3
+ Verifies the key against ``GET /v1/me`` and only then writes it to
4
+ ``~/.config/lighthouse/config.env`` (0600). The endpoint is ``--api-base``, then
5
+ ``LIGHTHOUSE_API_BASE_URL``, then the saved value, then the production default.
6
+ `init` reuses ``ensure_login`` so a first-time developer is prompted for a key inline.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+
12
+ from lighthouse_cli import ui
13
+ from lighthouse_cli.api import whoami
14
+ from lighthouse_cli.config import (
15
+ ClientConfig,
16
+ ClientConfigError,
17
+ load_client_config,
18
+ normalize_api_base_url,
19
+ resolve_api_base_url,
20
+ save_client_config,
21
+ )
22
+ from lighthouse_cli.http import ClientRequestError
23
+ from lighthouse_cli.parsing import parser
24
+
25
+
26
+ def _describe(identity: dict) -> str:
27
+ user = identity.get("user", {})
28
+ team = identity.get("team", {})
29
+ name = user.get("full_name") or user.get("email") or "your account"
30
+ role = user.get("role")
31
+ role_suffix = f" ({role})" if role else ""
32
+ return f"Signed in as {name}{role_suffix} · team {team.get('name', '?')}"
33
+
34
+
35
+ def resolve_and_verify(api_key: str, api_base_url: str) -> tuple[ClientConfig, dict]:
36
+ """Build a config, confirm it against the API, and return (config, identity)."""
37
+ config = ClientConfig(api_key=api_key, api_base_url=normalize_api_base_url(api_base_url))
38
+ identity = whoami(config)
39
+ return config, identity
40
+
41
+
42
+ def _verify_and_save(api_key: str, api_base_url: str) -> ClientConfig:
43
+ config, identity = resolve_and_verify(api_key, api_base_url)
44
+ path = save_client_config(config)
45
+ ui.success(_describe(identity))
46
+ ui.hint(f"Saved to {path}")
47
+ return config
48
+
49
+
50
+ def _prompt_key(needs: str) -> str:
51
+ api_key = ui.secret("API key (from your dashboard — reveal once)", needs=needs)
52
+ if not api_key:
53
+ raise ClientConfigError("An API key is required.")
54
+ return api_key
55
+
56
+
57
+ def ensure_login(*, assume_yes: bool = False) -> ClientConfig:
58
+ """Return a working config, prompting for a key if none is configured.
59
+
60
+ Raises ``ClientConfigError`` when non-interactive and no key is available.
61
+ """
62
+ try:
63
+ config = load_client_config()
64
+ whoami(config) # cheap check that the stored key still works
65
+ return config
66
+ except ClientConfigError:
67
+ if assume_yes:
68
+ raise
69
+ ui.warn("No Lighthouse API key configured yet.")
70
+ return _verify_and_save(
71
+ _prompt_key("LIGHTHOUSE_API_KEY or `login --api-key`"), resolve_api_base_url()
72
+ )
73
+
74
+
75
+ def run(argv: list[str] | None = None) -> int:
76
+ cli = parser("login", "Store and verify your API key.", "login --api-key <key>")
77
+ cli.add_argument(
78
+ "--api-key", default=None, help="API key (otherwise LIGHTHOUSE_API_KEY, or prompted)."
79
+ )
80
+ cli.add_argument("--api-base", default=None, help="Lighthouse API base URL.")
81
+ cli.add_argument("--yes", "-y", action="store_true", help="Never prompt.")
82
+ args = cli.parse_args(argv)
83
+
84
+ try:
85
+ api_base = args.api_base or resolve_api_base_url()
86
+ api_key = args.api_key or os.environ.get("LIGHTHOUSE_API_KEY") or ""
87
+ if not api_key:
88
+ if args.yes or sys.stdin is None or not sys.stdin.isatty():
89
+ ui.error("No API key. Pass --api-key or set LIGHTHOUSE_API_KEY.")
90
+ return 2
91
+ api_key = _prompt_key("--api-key or LIGHTHOUSE_API_KEY")
92
+ _verify_and_save(api_key, api_base)
93
+ except (ClientConfigError, ClientRequestError) as exc:
94
+ ui.error(str(exc))
95
+ return 1
96
+ return 0
97
+
98
+
99
+ __all__ = ["ensure_login", "resolve_and_verify", "run"]
@@ -0,0 +1,24 @@
1
+ """`reware-lighthouse logout` — remove the saved API key from this machine."""
2
+
3
+ import os
4
+
5
+ from lighthouse_cli import ui
6
+ from lighthouse_cli.config import ClientConfigError, clear_api_key
7
+ from lighthouse_cli.parsing import parser
8
+
9
+
10
+ def run(argv: list[str] | None = None) -> int:
11
+ parser("logout", "Remove the saved API key.", "logout").parse_args(argv)
12
+ try:
13
+ removed = clear_api_key()
14
+ except ClientConfigError as exc:
15
+ ui.error(str(exc))
16
+ return 1
17
+ ui.success("Removed the saved API key." if removed else "No saved API key to remove.")
18
+ if os.environ.get("LIGHTHOUSE_API_KEY"):
19
+ ui.warn("LIGHTHOUSE_API_KEY is set in your environment and stays active.")
20
+ ui.hint("Repository setup is unchanged. Revoke the key itself from the dashboard.")
21
+ return 0
22
+
23
+
24
+ __all__ = ["run"]