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.
- lighthouse_cli/__init__.py +9 -0
- lighthouse_cli/__main__.py +4 -0
- lighthouse_cli/api.py +39 -0
- lighthouse_cli/cli.py +100 -0
- lighthouse_cli/commands/__init__.py +1 -0
- lighthouse_cli/commands/deinit.py +57 -0
- lighthouse_cli/commands/init.py +137 -0
- lighthouse_cli/commands/login.py +99 -0
- lighthouse_cli/commands/logout.py +24 -0
- lighthouse_cli/commands/scan.py +229 -0
- lighthouse_cli/commands/status.py +119 -0
- lighthouse_cli/config.py +163 -0
- lighthouse_cli/git_remote.py +67 -0
- lighthouse_cli/hook.py +249 -0
- lighthouse_cli/http.py +144 -0
- lighthouse_cli/identity.py +22 -0
- lighthouse_cli/link.py +77 -0
- lighthouse_cli/mcp.py +126 -0
- lighthouse_cli/parsing.py +14 -0
- lighthouse_cli/resolution.py +100 -0
- lighthouse_cli/setup.py +704 -0
- lighthouse_cli/skill.py +53 -0
- lighthouse_cli/ui.py +180 -0
- reware_lighthouse-0.1.0.dist-info/METADATA +152 -0
- reware_lighthouse-0.1.0.dist-info/RECORD +28 -0
- reware_lighthouse-0.1.0.dist-info/WHEEL +4 -0
- reware_lighthouse-0.1.0.dist-info/entry_points.txt +2 -0
- reware_lighthouse-0.1.0.dist-info/licenses/LICENSE +13 -0
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"]
|