goodeye 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.
- goodeye-0.1.0.dist-info/METADATA +184 -0
- goodeye-0.1.0.dist-info/RECORD +21 -0
- goodeye-0.1.0.dist-info/WHEEL +4 -0
- goodeye-0.1.0.dist-info/entry_points.txt +2 -0
- goodeye-0.1.0.dist-info/licenses/LICENSE +21 -0
- goodeye_cli/__init__.py +3 -0
- goodeye_cli/__main__.py +16 -0
- goodeye_cli/app.py +69 -0
- goodeye_cli/auth_flows.py +168 -0
- goodeye_cli/client.py +299 -0
- goodeye_cli/commands/__init__.py +1 -0
- goodeye_cli/commands/auth.py +117 -0
- goodeye_cli/commands/design.py +47 -0
- goodeye_cli/commands/login.py +52 -0
- goodeye_cli/commands/logout.py +27 -0
- goodeye_cli/commands/signup.py +29 -0
- goodeye_cli/commands/skills.py +249 -0
- goodeye_cli/commands/whoami.py +35 -0
- goodeye_cli/config.py +146 -0
- goodeye_cli/errors.py +98 -0
- goodeye_cli/wire.py +116 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: goodeye
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Public CLI for Goodeye - manage AI workflow skills from the terminal.
|
|
5
|
+
Project-URL: Repository, https://github.com/Goodeye-Labs/goodeye-cli
|
|
6
|
+
Project-URL: Issues, https://github.com/Goodeye-Labs/goodeye-cli/issues
|
|
7
|
+
Author: Goodeye Labs
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Goodeye Labs
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: ai,cli,goodeye,mcp,workflows
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Environment :: Console
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Topic :: Software Development
|
|
39
|
+
Classifier: Topic :: Utilities
|
|
40
|
+
Requires-Python: >=3.12
|
|
41
|
+
Requires-Dist: httpx>=0.27
|
|
42
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
43
|
+
Requires-Dist: pydantic>=2.7
|
|
44
|
+
Requires-Dist: pyyaml>=6
|
|
45
|
+
Requires-Dist: rich>=13
|
|
46
|
+
Requires-Dist: typer>=0.12
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# goodeye-cli
|
|
50
|
+
|
|
51
|
+
Command-line client for Goodeye - manage AI workflow skills from the terminal.
|
|
52
|
+
|
|
53
|
+
Goodeye is an outcome-aligned AI workflow registry: you author skills (markdown
|
|
54
|
+
bodies + manifests) and verifiers that score an AI agent against a measurable
|
|
55
|
+
business outcome. This CLI is wired to the public `/v1/` REST API.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
Requires Python 3.12+.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
uv tool install goodeye
|
|
63
|
+
# or
|
|
64
|
+
pipx install goodeye
|
|
65
|
+
# or
|
|
66
|
+
pip install goodeye
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Once installed, the `goodeye` command is available on your `PATH`.
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
# Browse the public registry without an account
|
|
75
|
+
goodeye skills list --filter public
|
|
76
|
+
|
|
77
|
+
# Create an account (emails a one-time code)
|
|
78
|
+
goodeye signup --email you@example.com
|
|
79
|
+
|
|
80
|
+
# Or log in on a machine with a browser
|
|
81
|
+
goodeye login
|
|
82
|
+
|
|
83
|
+
# Confirm who you are
|
|
84
|
+
goodeye whoami
|
|
85
|
+
|
|
86
|
+
# Fetch a public skill as markdown
|
|
87
|
+
goodeye skills get brand-voice > brand-voice.md
|
|
88
|
+
|
|
89
|
+
# Push a local skill
|
|
90
|
+
goodeye skills push ./my-skill.md --public
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Skill files
|
|
94
|
+
|
|
95
|
+
`goodeye skills push` reads a markdown file with optional YAML front-matter:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+
---
|
|
99
|
+
slug: my-skill
|
|
100
|
+
visibility: private
|
|
101
|
+
manifest:
|
|
102
|
+
title: My skill
|
|
103
|
+
tags: [data, cleanup]
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
# Body
|
|
107
|
+
|
|
108
|
+
The rest of the file is the skill body rendered to the agent at runtime.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--public` on the command line overrides `visibility`. `--slug` on the command
|
|
112
|
+
line overrides the front-matter `slug`.
|
|
113
|
+
|
|
114
|
+
## Command reference
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
goodeye login [--email EMAIL]
|
|
118
|
+
Without --email: opens the browser for WorkOS device-code approval.
|
|
119
|
+
With --email: sends a one-time code to your inbox.
|
|
120
|
+
|
|
121
|
+
goodeye signup --email EMAIL
|
|
122
|
+
Creates an account and mints your initial API key.
|
|
123
|
+
|
|
124
|
+
goodeye logout
|
|
125
|
+
Deletes local credentials. Does not revoke the key server-side; see
|
|
126
|
+
`goodeye auth list-keys` and `goodeye auth revoke-key`.
|
|
127
|
+
|
|
128
|
+
goodeye whoami
|
|
129
|
+
Shows the current user identified by your credentials.
|
|
130
|
+
|
|
131
|
+
goodeye auth create-key --name NAME [--copy]
|
|
132
|
+
Mints a new API key. The secret is printed once.
|
|
133
|
+
|
|
134
|
+
goodeye auth list-keys
|
|
135
|
+
Table of your API keys (secrets are never returned).
|
|
136
|
+
|
|
137
|
+
goodeye auth revoke-key <id>
|
|
138
|
+
Revokes (soft-deletes) a key.
|
|
139
|
+
|
|
140
|
+
goodeye skills list [--filter all|public|own] [--tag TAG] [--search QUERY] [--json]
|
|
141
|
+
Paginated listing; auto-follows cursor.
|
|
142
|
+
|
|
143
|
+
goodeye skills get <id-or-slug> [--version N] [--output PATH] [--json]
|
|
144
|
+
Emits raw markdown by default; --json returns the envelope.
|
|
145
|
+
|
|
146
|
+
goodeye skills push <file.md> [--id ID] [--public] [--slug SLUG]
|
|
147
|
+
Creates or appends a skill version.
|
|
148
|
+
|
|
149
|
+
goodeye skills set-visibility <id> <private|public>
|
|
150
|
+
goodeye skills delete <id> [--yes]
|
|
151
|
+
|
|
152
|
+
goodeye design
|
|
153
|
+
Prints the workflow-designer prompt pack to stdout.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
### Credentials
|
|
159
|
+
|
|
160
|
+
- `GOODEYE_API_KEY` env var (highest precedence).
|
|
161
|
+
- `~/.config/goodeye/credentials.json` (or `$XDG_CONFIG_HOME/goodeye/`).
|
|
162
|
+
|
|
163
|
+
Credential files are created with mode `0600`.
|
|
164
|
+
|
|
165
|
+
### Server
|
|
166
|
+
|
|
167
|
+
- `GOODEYE_SERVER` env var.
|
|
168
|
+
- `server` field inside `credentials.json`.
|
|
169
|
+
- Default: `https://mcp.goodeyelabs.com`.
|
|
170
|
+
|
|
171
|
+
## REST API, not the CLI
|
|
172
|
+
|
|
173
|
+
This CLI is pinned to the `/v1/` REST API contract. If you are integrating
|
|
174
|
+
programmatically and want a stable contract, prefer the REST API directly;
|
|
175
|
+
the CLI is a convenience layer over it.
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for local-dev setup and the PR
|
|
180
|
+
process. Issues and PRs welcome.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
goodeye_cli/__init__.py,sha256=RSSlXudxAflV6gv0bxSl7812iWOeg5CE9rVH4kkRqIA,53
|
|
2
|
+
goodeye_cli/__main__.py,sha256=mi3sTSnf-KIOp1YSjj7rd4bdIh6uZ014YmqkIeMwQi8,238
|
|
3
|
+
goodeye_cli/app.py,sha256=PFdsdxja4TQM-eGyJ5WM1f4hlNRjP5OBndjeFqSTwlA,2056
|
|
4
|
+
goodeye_cli/auth_flows.py,sha256=iXjNAOaepYXh57BWjHTD7bd-P_V8Pjclg42MYw07lSg,6367
|
|
5
|
+
goodeye_cli/client.py,sha256=xATishplzeJGxKoiOpwaolXLaRyH25mTQit3pjJwRcI,9927
|
|
6
|
+
goodeye_cli/config.py,sha256=QD6klJQ3iy07IDtuSJjwA3gECwglcjzzVj7aHoVg-tY,4832
|
|
7
|
+
goodeye_cli/errors.py,sha256=reJpX5gSYa5SKAPtUoA3ZQVQzvlORh2tq0SQ_9ioN2U,2849
|
|
8
|
+
goodeye_cli/wire.py,sha256=GFbdZFfqolcB-Za2OBZ9JmvPCnD7KWTr_0lKH5-rmvo,2256
|
|
9
|
+
goodeye_cli/commands/__init__.py,sha256=lt7T0WVXVuhXPgzvaCmzBnCB21N1d0G-u2iJ5unxuCA,32
|
|
10
|
+
goodeye_cli/commands/auth.py,sha256=jBfj2QSRskCuT1lYdb8RCoudI2Mowd-Qfs2jWmEcbDM,3827
|
|
11
|
+
goodeye_cli/commands/design.py,sha256=otAVFVk87wl9YyaxiGhdUVZEs_lr2EMlu5yxQX-1S8U,1478
|
|
12
|
+
goodeye_cli/commands/login.py,sha256=IapCLBccg4_MbwGnf5Ard2-pLLcJbI2n2tl9rhdE3to,1747
|
|
13
|
+
goodeye_cli/commands/logout.py,sha256=MIhapOGWrsoaa-Z0GrIZTezP7OeUsssY5Iu1J059gOI,902
|
|
14
|
+
goodeye_cli/commands/signup.py,sha256=m3qa2kGl223AP2k_zF9laG9IuDgXfnePe0QWq2FJnSI,843
|
|
15
|
+
goodeye_cli/commands/skills.py,sha256=wPZuztOZLYp0Iu85C1CCaM8yH20Iuj1MrKaBy1sLN7U,8439
|
|
16
|
+
goodeye_cli/commands/whoami.py,sha256=agN_cTuk8yL5rbVqd5y_FgzyUcxhtDXxFM0Le_mTUIY,994
|
|
17
|
+
goodeye-0.1.0.dist-info/METADATA,sha256=-wUGoQjNO3Lhev7QXvLxR0zVi8eECkaren8GRLtUT_A,5564
|
|
18
|
+
goodeye-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
19
|
+
goodeye-0.1.0.dist-info/entry_points.txt,sha256=is74w4li0ittV5GT7DagyP_qS5MHHGyA6qZZw3qtzhI,54
|
|
20
|
+
goodeye-0.1.0.dist-info/licenses/LICENSE,sha256=elL9v1FdiFNSU5YJe1YWYjlJ0EqHbvUxINlVOOZytr4,1069
|
|
21
|
+
goodeye-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Goodeye Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
goodeye_cli/__init__.py
ADDED
goodeye_cli/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Module entrypoint.
|
|
2
|
+
|
|
3
|
+
Enables ``python -m goodeye_cli`` and the ``goodeye`` console script.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from goodeye_cli.app import app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
app()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
goodeye_cli/app.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Typer app root. Wires subcommands and a global error handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from goodeye_cli import __version__
|
|
11
|
+
from goodeye_cli.commands import auth as auth_cmds
|
|
12
|
+
from goodeye_cli.commands import design as design_cmd
|
|
13
|
+
from goodeye_cli.commands import login as login_cmd
|
|
14
|
+
from goodeye_cli.commands import logout as logout_cmd
|
|
15
|
+
from goodeye_cli.commands import signup as signup_cmd
|
|
16
|
+
from goodeye_cli.commands import skills as skills_cmds
|
|
17
|
+
from goodeye_cli.commands import whoami as whoami_cmd
|
|
18
|
+
from goodeye_cli.errors import GoodeyeError
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="goodeye",
|
|
22
|
+
help="Public CLI for Goodeye - manage AI workflow skills from the terminal.",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Top-level commands.
|
|
28
|
+
app.command("login")(login_cmd.login)
|
|
29
|
+
app.command("signup")(signup_cmd.signup)
|
|
30
|
+
app.command("logout")(logout_cmd.logout)
|
|
31
|
+
app.command("whoami")(whoami_cmd.whoami)
|
|
32
|
+
app.command("design")(design_cmd.design)
|
|
33
|
+
|
|
34
|
+
# Command groups.
|
|
35
|
+
app.add_typer(auth_cmds.app, name="auth", help="Manage API keys.")
|
|
36
|
+
app.add_typer(skills_cmds.app, name="skills", help="Browse and manage skills.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _version_callback(value: bool) -> None:
|
|
40
|
+
if value:
|
|
41
|
+
typer.echo(f"goodeye {__version__}")
|
|
42
|
+
raise typer.Exit()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.callback()
|
|
46
|
+
def _root(
|
|
47
|
+
version: bool = typer.Option(
|
|
48
|
+
False,
|
|
49
|
+
"--version",
|
|
50
|
+
callback=_version_callback,
|
|
51
|
+
is_eager=True,
|
|
52
|
+
help="Show the CLI version and exit.",
|
|
53
|
+
),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Global options processed before any subcommand."""
|
|
56
|
+
# Body intentionally empty; the callback fires only to register the option.
|
|
57
|
+
_ = version
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
"""Console-script entrypoint with a structured-error-friendly wrapper."""
|
|
62
|
+
console = Console(stderr=True)
|
|
63
|
+
try:
|
|
64
|
+
app()
|
|
65
|
+
except GoodeyeError as exc:
|
|
66
|
+
console.print(f"[bold red]{exc.slug}[/bold red]: {exc.message}")
|
|
67
|
+
if exc.hint:
|
|
68
|
+
console.print(f"[dim]hint: {exc.hint}[/dim]")
|
|
69
|
+
sys.exit(1)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Browser-assisted and headless auth flows.
|
|
2
|
+
|
|
3
|
+
Two flows are implemented:
|
|
4
|
+
|
|
5
|
+
* **Device code flow** (``device_code_login``): used for ``goodeye login`` with no
|
|
6
|
+
email. Requests a user_code from WorkOS, opens the verification URL in the
|
|
7
|
+
default browser, polls until the user approves, then exchanges the resulting
|
|
8
|
+
WorkOS JWT for a Goodeye API key via ``POST /v1/auth/exchange``.
|
|
9
|
+
|
|
10
|
+
* **Magic-auth flow** (``magic_auth_flow``): used for ``goodeye login --email``
|
|
11
|
+
and ``goodeye signup --email``. Posts the email to ``/v1/{intent}``, prompts
|
|
12
|
+
for the emailed code, and posts to ``/v1/{intent}/verify`` to retrieve the
|
|
13
|
+
initial API key.
|
|
14
|
+
|
|
15
|
+
The flows are deliberately injection-friendly: every external dependency
|
|
16
|
+
(``httpx`` transport, browser opener, code prompt, clock) can be overridden so
|
|
17
|
+
the flows can be unit-tested with ``respx`` without any real I/O.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import contextlib
|
|
23
|
+
import time
|
|
24
|
+
import webbrowser
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
|
|
29
|
+
from goodeye_cli.client import (
|
|
30
|
+
GoodeyeClient,
|
|
31
|
+
poll_device_token,
|
|
32
|
+
request_device_authorization,
|
|
33
|
+
)
|
|
34
|
+
from goodeye_cli.errors import GoodeyeError, InvalidCredentials
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def device_code_login(
|
|
38
|
+
server: str,
|
|
39
|
+
workos_client_id: str,
|
|
40
|
+
workos_device_authorization_uri: str,
|
|
41
|
+
workos_token_uri: str,
|
|
42
|
+
*,
|
|
43
|
+
hostname: str | None = None,
|
|
44
|
+
console: Console | None = None,
|
|
45
|
+
open_browser: Callable[[str], bool] | None = None,
|
|
46
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
47
|
+
clock: Callable[[], float] = time.monotonic,
|
|
48
|
+
max_wait_s: float | None = None,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Run the device-code flow and return the minted API key.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
server: Goodeye server base URL (used for ``/v1/auth/exchange``).
|
|
54
|
+
workos_client_id: The WorkOS application client id, fetched from
|
|
55
|
+
``/.well-known/goodeye-client-config``.
|
|
56
|
+
workos_device_authorization_uri: WorkOS device-authorization endpoint URL.
|
|
57
|
+
workos_token_uri: WorkOS token endpoint URL.
|
|
58
|
+
hostname: Optional host label to embed in the minted key's name.
|
|
59
|
+
console: Rich console for UX output. A default is created when None.
|
|
60
|
+
open_browser: Overridable function to open the verification URL.
|
|
61
|
+
Returns False if no browser could be opened.
|
|
62
|
+
sleep: Overridable sleep for tests.
|
|
63
|
+
clock: Overridable monotonic clock for tests.
|
|
64
|
+
max_wait_s: Optional hard cap on total poll duration. Defaults to the
|
|
65
|
+
``expires_in`` returned by WorkOS.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The newly minted Goodeye API key.
|
|
69
|
+
"""
|
|
70
|
+
out = console or Console()
|
|
71
|
+
opener = open_browser or webbrowser.open
|
|
72
|
+
|
|
73
|
+
auth = request_device_authorization(workos_device_authorization_uri, workos_client_id)
|
|
74
|
+
out.print(
|
|
75
|
+
f"\nVisit this URL to approve the sign-in:\n [bold]{auth.verification_uri_complete}[/bold]"
|
|
76
|
+
)
|
|
77
|
+
out.print(f"User code: [bold]{auth.user_code}[/bold]\n")
|
|
78
|
+
# Best-effort browser open; failure is non-fatal because the URL is already printed.
|
|
79
|
+
with contextlib.suppress(Exception):
|
|
80
|
+
opener(auth.verification_uri_complete)
|
|
81
|
+
|
|
82
|
+
deadline = clock() + (max_wait_s if max_wait_s is not None else auth.expires_in)
|
|
83
|
+
interval = max(1, int(auth.interval))
|
|
84
|
+
|
|
85
|
+
access_token: str | None = None
|
|
86
|
+
while clock() < deadline:
|
|
87
|
+
status, body = poll_device_token(workos_token_uri, workos_client_id, auth.device_code)
|
|
88
|
+
if status == 200 and isinstance(body.get("access_token"), str):
|
|
89
|
+
access_token = str(body["access_token"])
|
|
90
|
+
break
|
|
91
|
+
# WorkOS/OAuth pending-states: continue polling. Everything else: fail fast.
|
|
92
|
+
error = body.get("error") if isinstance(body, dict) else None
|
|
93
|
+
if status == 400 and error in ("authorization_pending", "slow_down"):
|
|
94
|
+
if error == "slow_down":
|
|
95
|
+
interval += 5
|
|
96
|
+
sleep(interval)
|
|
97
|
+
continue
|
|
98
|
+
description = body.get("error_description") if isinstance(body, dict) else None
|
|
99
|
+
message = description if isinstance(description, str) else "Device authorization failed."
|
|
100
|
+
raise InvalidCredentials(
|
|
101
|
+
slug=str(error) if isinstance(error, str) else "invalid_credentials",
|
|
102
|
+
message=message,
|
|
103
|
+
status_code=status,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if access_token is None:
|
|
107
|
+
raise GoodeyeError(
|
|
108
|
+
slug="auth_required",
|
|
109
|
+
message="Timed out waiting for device approval.",
|
|
110
|
+
hint="Re-run `goodeye login` and complete approval in the browser.",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with GoodeyeClient(server, api_key=access_token) as client:
|
|
114
|
+
result = client.exchange(hostname=hostname)
|
|
115
|
+
return result.api_key
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def magic_auth_flow(
|
|
119
|
+
server: str,
|
|
120
|
+
email: str,
|
|
121
|
+
*,
|
|
122
|
+
intent: str = "login",
|
|
123
|
+
prompt_code: Callable[[], str] | None = None,
|
|
124
|
+
console: Console | None = None,
|
|
125
|
+
) -> str:
|
|
126
|
+
"""Run the magic-auth flow and return the newly minted API key.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
server: Goodeye server base URL.
|
|
130
|
+
email: The user's email address.
|
|
131
|
+
intent: ``"login"`` or ``"signup"``. Both call the same underlying WorkOS
|
|
132
|
+
magic-auth endpoints but are kept separate for logging/legibility.
|
|
133
|
+
prompt_code: Callable that returns the 6-digit code the user received.
|
|
134
|
+
Defaults to a Rich input prompt.
|
|
135
|
+
console: Rich console for UX output.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The newly minted Goodeye API key.
|
|
139
|
+
"""
|
|
140
|
+
if intent not in ("login", "signup"):
|
|
141
|
+
raise ValueError(f"intent must be 'login' or 'signup', got {intent!r}")
|
|
142
|
+
|
|
143
|
+
out = console or Console()
|
|
144
|
+
|
|
145
|
+
def _default_prompt() -> str:
|
|
146
|
+
return out.input("Enter the 6-digit code sent to your email: ").strip()
|
|
147
|
+
|
|
148
|
+
prompt = prompt_code or _default_prompt
|
|
149
|
+
|
|
150
|
+
with GoodeyeClient(server) as client:
|
|
151
|
+
if intent == "signup":
|
|
152
|
+
client.signup(email)
|
|
153
|
+
else:
|
|
154
|
+
client.login(email)
|
|
155
|
+
out.print(f"A sign-in code was sent to [bold]{email}[/bold].")
|
|
156
|
+
code = prompt()
|
|
157
|
+
if not code:
|
|
158
|
+
raise InvalidCredentials(
|
|
159
|
+
slug="invalid_credentials",
|
|
160
|
+
message="No code provided.",
|
|
161
|
+
hint="Check your email and re-run the command.",
|
|
162
|
+
)
|
|
163
|
+
result = (
|
|
164
|
+
client.signup_verify(email, code)
|
|
165
|
+
if intent == "signup"
|
|
166
|
+
else client.login_verify(email, code)
|
|
167
|
+
)
|
|
168
|
+
return result.api_key
|