minitest-cli 0.4.4__tar.gz → 0.4.5__tar.gz
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.
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/PKG-INFO +39 -33
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/README.md +38 -32
- minitest_cli-0.4.5/install.sh +73 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/pyproject.toml +1 -1
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/auth.py +52 -1
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/oauth.py +27 -28
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/token_exchange.py +38 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/uv.lock +1 -1
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/.env.example +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/.github/workflows/ci.yml +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/.github/workflows/release.yml +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/.gitignore +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/.opencode/skill/release/SKILL.md +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/AGENTS.md +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/RELEASE.md +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/pyrightconfig.json +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/api/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/api/client.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/assets/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/assets/callback.html +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/apps.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/build.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/build_helpers.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/flow.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/flow_helpers.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/flow_modify.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/run.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/run_display.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/run_helpers.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/commands/skill.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/app_context.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/auth.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/config.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/core/credentials.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/main.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/models/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/models/app.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/models/base.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/models/build.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/models/flow_run.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/models/flow_template.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/utils/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/utils/output.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/src/minitest_cli/utils/update_check.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/__init__.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_apps_commands.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_auth.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_auth_commands.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_build_commands.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_code_quality.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_flow_commands.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_run_commands.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_skill_command.py +0 -0
- {minitest_cli-0.4.4 → minitest_cli-0.4.5}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: minitest-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Minitest CLI – command-line interface for the Minitest testing platform
|
|
5
5
|
Project-URL: Homepage, https://minitap.ai/
|
|
6
6
|
Project-URL: Source, https://github.com/minitap-ai/minitest-cli
|
|
@@ -37,24 +37,30 @@ Command-line interface for the Minitest testing platform.
|
|
|
37
37
|
|
|
38
38
|
## Installation
|
|
39
39
|
|
|
40
|
-
###
|
|
40
|
+
### curl (recommended)
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
|
|
43
|
+
curl -fsSL https://raw.githubusercontent.com/minitap-ai/minitest-cli/main/install.sh | bash
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Homebrew
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
```bash
|
|
49
|
+
brew install minitap-ai/tap/minitest-cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### pip
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
|
-
|
|
55
|
+
python3 -m pip install --user minitest-cli
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
###
|
|
58
|
+
### uvx (zero-install)
|
|
59
|
+
|
|
60
|
+
Run without installing:
|
|
55
61
|
|
|
56
62
|
```bash
|
|
57
|
-
|
|
63
|
+
uvx --from minitest-cli minitest --help
|
|
58
64
|
```
|
|
59
65
|
|
|
60
66
|
### From source
|
|
@@ -81,40 +87,40 @@ minitest run --app <app-id>
|
|
|
81
87
|
|
|
82
88
|
## Configuration
|
|
83
89
|
|
|
84
|
-
| Environment Variable | Description
|
|
85
|
-
|
|
86
|
-
| `MINITEST_TOKEN`
|
|
87
|
-
| `MINITEST_APP_ID`
|
|
88
|
-
| `MINITEST_API_URL`
|
|
90
|
+
| Environment Variable | Description | Required |
|
|
91
|
+
| -------------------- | ------------------------ | ---------------------------------- |
|
|
92
|
+
| `MINITEST_TOKEN` | API authentication token | Yes (or use `minitest auth login`) |
|
|
93
|
+
| `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
|
|
94
|
+
| `MINITEST_API_URL` | API base URL | No (defaults to production) |
|
|
89
95
|
|
|
90
96
|
## Global Flags
|
|
91
97
|
|
|
92
|
-
| Flag
|
|
93
|
-
|
|
94
|
-
| `--json`
|
|
95
|
-
| `--app <id-or-name>` | Target app for commands that require one
|
|
96
|
-
| `--version`
|
|
97
|
-
| `--help`
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
| -------------------- | ------------------------------------------------ |
|
|
100
|
+
| `--json` | Output JSON to stdout (diagnostics go to stderr) |
|
|
101
|
+
| `--app <id-or-name>` | Target app for commands that require one |
|
|
102
|
+
| `--version` | Show CLI version |
|
|
103
|
+
| `--help` | Show help |
|
|
98
104
|
|
|
99
105
|
## Commands
|
|
100
106
|
|
|
101
|
-
| Command
|
|
102
|
-
|
|
103
|
-
| `minitest auth`
|
|
104
|
-
| `minitest apps`
|
|
105
|
-
| `minitest flow`
|
|
106
|
-
| `minitest build` | Build management
|
|
107
|
-
| `minitest run`
|
|
107
|
+
| Command | Description |
|
|
108
|
+
| ---------------- | ------------------------- |
|
|
109
|
+
| `minitest auth` | Authentication management |
|
|
110
|
+
| `minitest apps` | App management |
|
|
111
|
+
| `minitest flow` | Testing flow operations |
|
|
112
|
+
| `minitest build` | Build management |
|
|
113
|
+
| `minitest run` | Test execution |
|
|
108
114
|
|
|
109
115
|
## Exit Codes
|
|
110
116
|
|
|
111
|
-
| Code | Meaning
|
|
112
|
-
|
|
113
|
-
| 0
|
|
114
|
-
| 1
|
|
115
|
-
| 2
|
|
116
|
-
| 3
|
|
117
|
-
| 4
|
|
117
|
+
| Code | Meaning |
|
|
118
|
+
| ---- | -------------------- |
|
|
119
|
+
| 0 | Success |
|
|
120
|
+
| 1 | General error |
|
|
121
|
+
| 2 | Authentication error |
|
|
122
|
+
| 3 | Network / API error |
|
|
123
|
+
| 4 | Resource not found |
|
|
118
124
|
|
|
119
125
|
## Using the Dev Environment
|
|
120
126
|
|
|
@@ -4,24 +4,30 @@ Command-line interface for the Minitest testing platform.
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### curl (recommended)
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
curl -fsSL https://raw.githubusercontent.com/minitap-ai/minitest-cli/main/install.sh | bash
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Homebrew
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```bash
|
|
16
|
+
brew install minitap-ai/tap/minitest-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### pip
|
|
16
20
|
|
|
17
21
|
```bash
|
|
18
|
-
|
|
22
|
+
python3 -m pip install --user minitest-cli
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
###
|
|
25
|
+
### uvx (zero-install)
|
|
26
|
+
|
|
27
|
+
Run without installing:
|
|
22
28
|
|
|
23
29
|
```bash
|
|
24
|
-
|
|
30
|
+
uvx --from minitest-cli minitest --help
|
|
25
31
|
```
|
|
26
32
|
|
|
27
33
|
### From source
|
|
@@ -48,40 +54,40 @@ minitest run --app <app-id>
|
|
|
48
54
|
|
|
49
55
|
## Configuration
|
|
50
56
|
|
|
51
|
-
| Environment Variable | Description
|
|
52
|
-
|
|
53
|
-
| `MINITEST_TOKEN`
|
|
54
|
-
| `MINITEST_APP_ID`
|
|
55
|
-
| `MINITEST_API_URL`
|
|
57
|
+
| Environment Variable | Description | Required |
|
|
58
|
+
| -------------------- | ------------------------ | ---------------------------------- |
|
|
59
|
+
| `MINITEST_TOKEN` | API authentication token | Yes (or use `minitest auth login`) |
|
|
60
|
+
| `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
|
|
61
|
+
| `MINITEST_API_URL` | API base URL | No (defaults to production) |
|
|
56
62
|
|
|
57
63
|
## Global Flags
|
|
58
64
|
|
|
59
|
-
| Flag
|
|
60
|
-
|
|
61
|
-
| `--json`
|
|
62
|
-
| `--app <id-or-name>` | Target app for commands that require one
|
|
63
|
-
| `--version`
|
|
64
|
-
| `--help`
|
|
65
|
+
| Flag | Description |
|
|
66
|
+
| -------------------- | ------------------------------------------------ |
|
|
67
|
+
| `--json` | Output JSON to stdout (diagnostics go to stderr) |
|
|
68
|
+
| `--app <id-or-name>` | Target app for commands that require one |
|
|
69
|
+
| `--version` | Show CLI version |
|
|
70
|
+
| `--help` | Show help |
|
|
65
71
|
|
|
66
72
|
## Commands
|
|
67
73
|
|
|
68
|
-
| Command
|
|
69
|
-
|
|
70
|
-
| `minitest auth`
|
|
71
|
-
| `minitest apps`
|
|
72
|
-
| `minitest flow`
|
|
73
|
-
| `minitest build` | Build management
|
|
74
|
-
| `minitest run`
|
|
74
|
+
| Command | Description |
|
|
75
|
+
| ---------------- | ------------------------- |
|
|
76
|
+
| `minitest auth` | Authentication management |
|
|
77
|
+
| `minitest apps` | App management |
|
|
78
|
+
| `minitest flow` | Testing flow operations |
|
|
79
|
+
| `minitest build` | Build management |
|
|
80
|
+
| `minitest run` | Test execution |
|
|
75
81
|
|
|
76
82
|
## Exit Codes
|
|
77
83
|
|
|
78
|
-
| Code | Meaning
|
|
79
|
-
|
|
80
|
-
| 0
|
|
81
|
-
| 1
|
|
82
|
-
| 2
|
|
83
|
-
| 3
|
|
84
|
-
| 4
|
|
84
|
+
| Code | Meaning |
|
|
85
|
+
| ---- | -------------------- |
|
|
86
|
+
| 0 | Success |
|
|
87
|
+
| 1 | General error |
|
|
88
|
+
| 2 | Authentication error |
|
|
89
|
+
| 3 | Network / API error |
|
|
90
|
+
| 4 | Resource not found |
|
|
85
91
|
|
|
86
92
|
## Using the Dev Environment
|
|
87
93
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install.sh — Install minitest-cli (brew → pipx → python3 -m pip fallback)
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# curl -fsSL https://raw.githubusercontent.com/minitap-ai/minitest-cli/main/install.sh | bash
|
|
6
|
+
#
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
PACKAGE="minitest-cli"
|
|
10
|
+
BREW_TAP="minitap-ai/tap/minitest-cli"
|
|
11
|
+
INSTALLED_VIA=""
|
|
12
|
+
|
|
13
|
+
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
|
14
|
+
warn() { printf '\033[1;33mWarning:\033[0m %s\n' "$*"; }
|
|
15
|
+
error() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; }
|
|
16
|
+
|
|
17
|
+
# -------------------------------------------------------------------
|
|
18
|
+
# 1. Try installers in order: brew → pipx → python3 -m pip
|
|
19
|
+
# Each attempt is conditional — failure falls through to the next.
|
|
20
|
+
# -------------------------------------------------------------------
|
|
21
|
+
if command -v brew &>/dev/null; then
|
|
22
|
+
info "Installing $PACKAGE with Homebrew…"
|
|
23
|
+
if brew install "$BREW_TAP"; then
|
|
24
|
+
INSTALLED_VIA="brew"
|
|
25
|
+
else
|
|
26
|
+
warn "Homebrew install failed — trying next method."
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [[ -z "$INSTALLED_VIA" ]] && command -v pipx &>/dev/null; then
|
|
31
|
+
info "Installing $PACKAGE with pipx…"
|
|
32
|
+
if pipx install "$PACKAGE" --force; then
|
|
33
|
+
INSTALLED_VIA="pipx"
|
|
34
|
+
else
|
|
35
|
+
warn "pipx install failed — trying next method."
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ -z "$INSTALLED_VIA" ]] && command -v python3 &>/dev/null; then
|
|
40
|
+
info "Installing $PACKAGE with python3 -m pip…"
|
|
41
|
+
if python3 -m pip install --user "$PACKAGE"; then
|
|
42
|
+
INSTALLED_VIA="pip"
|
|
43
|
+
else
|
|
44
|
+
warn "pip install failed."
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [[ -z "$INSTALLED_VIA" ]]; then
|
|
49
|
+
error "All install methods failed or no supported package manager found."
|
|
50
|
+
error " macOS: Install Homebrew → https://brew.sh"
|
|
51
|
+
error " Linux: sudo apt install pipx && pipx ensurepath"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# -------------------------------------------------------------------
|
|
56
|
+
# 2. Verify installation
|
|
57
|
+
# -------------------------------------------------------------------
|
|
58
|
+
if command -v minitest &>/dev/null; then
|
|
59
|
+
info "minitest-cli installed successfully via $INSTALLED_VIA! 🎉"
|
|
60
|
+
minitest --version
|
|
61
|
+
echo ""
|
|
62
|
+
info "Next steps:"
|
|
63
|
+
echo " minitest auth login # authenticate"
|
|
64
|
+
echo " minitest apps list # list your apps"
|
|
65
|
+
echo " minitest --help # see all commands"
|
|
66
|
+
else
|
|
67
|
+
warn "Installation completed, but 'minitest' is not on your PATH."
|
|
68
|
+
if [[ "$INSTALLED_VIA" == "brew" ]]; then
|
|
69
|
+
warn "Try restarting your shell or check: brew --prefix"
|
|
70
|
+
else
|
|
71
|
+
warn "You may need to restart your shell or add ~/.local/bin to your PATH."
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Authentication commands: login, logout, status."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
3
5
|
from datetime import UTC, datetime
|
|
4
6
|
|
|
5
7
|
import typer
|
|
@@ -13,10 +15,37 @@ from minitest_cli.core.auth import (
|
|
|
13
15
|
oauth_pkce_login,
|
|
14
16
|
)
|
|
15
17
|
from minitest_cli.core.config import Settings
|
|
16
|
-
from minitest_cli.utils.output import output, print_error, print_success
|
|
18
|
+
from minitest_cli.utils.output import output, print_error, print_info, print_success
|
|
17
19
|
|
|
18
20
|
app = typer.Typer(name="auth", help="Authentication management.")
|
|
19
21
|
|
|
22
|
+
SKILL_NAME = "minitest-cli"
|
|
23
|
+
SKILL_INSTALL_CMD = "npx skills add minitap-ai/agent-skills --skill minitest-cli"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_skill_installed() -> bool:
|
|
27
|
+
"""Check if the minitest-cli skill is installed via ``npx skills ls``.
|
|
28
|
+
|
|
29
|
+
Queries both project-level and global scopes so the detection stays in
|
|
30
|
+
sync with whatever directories the ``skills`` CLI manages.
|
|
31
|
+
"""
|
|
32
|
+
for flags in (["--json"], ["--json", "-g"]):
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["npx", "skills", "ls", *flags],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=30,
|
|
39
|
+
)
|
|
40
|
+
if result.returncode == 0:
|
|
41
|
+
skills = json.loads(result.stdout)
|
|
42
|
+
if any(s.get("name") == SKILL_NAME for s in skills):
|
|
43
|
+
return True
|
|
44
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError, OSError):
|
|
45
|
+
# npx not available or unexpected output – fall through
|
|
46
|
+
pass
|
|
47
|
+
return False
|
|
48
|
+
|
|
20
49
|
|
|
21
50
|
def _get_settings() -> Settings:
|
|
22
51
|
"""Retrieve settings stored by the main callback."""
|
|
@@ -40,6 +69,28 @@ def login() -> None:
|
|
|
40
69
|
creds = oauth_pkce_login(settings)
|
|
41
70
|
print_success(f"Authenticated as {creds.email}")
|
|
42
71
|
|
|
72
|
+
# Check if the minitest-cli agent skill is installed
|
|
73
|
+
if not _is_skill_installed():
|
|
74
|
+
print_info("")
|
|
75
|
+
print_info("💡 The minitest-cli agent skill is not installed in this project.")
|
|
76
|
+
print_info(" Your AI agent needs it to know how to use minitest.")
|
|
77
|
+
print_info("")
|
|
78
|
+
try:
|
|
79
|
+
answer = input(" Install it now? [Y/n] ").strip().lower()
|
|
80
|
+
except (EOFError, KeyboardInterrupt):
|
|
81
|
+
answer = "n"
|
|
82
|
+
print() # newline after ^C / ^D
|
|
83
|
+
if answer in ("", "y", "yes"):
|
|
84
|
+
print_info("")
|
|
85
|
+
print_info(f" Running: {SKILL_INSTALL_CMD}")
|
|
86
|
+
print_info("")
|
|
87
|
+
subprocess.run(SKILL_INSTALL_CMD.split(), check=False)
|
|
88
|
+
else:
|
|
89
|
+
print_info("")
|
|
90
|
+
print_info(" You can install it later with:")
|
|
91
|
+
print_info(f" {SKILL_INSTALL_CMD}")
|
|
92
|
+
print_info("")
|
|
93
|
+
|
|
43
94
|
|
|
44
95
|
@app.command()
|
|
45
96
|
def logout() -> None:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""OAuth PKCE login flow and token refresh."""
|
|
1
|
+
"""OAuth PKCE login flow via Supabase OAuth2 server, and token refresh."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -20,9 +20,8 @@ from minitest_cli.core.config import Settings
|
|
|
20
20
|
from minitest_cli.core.credentials import Credentials
|
|
21
21
|
from minitest_cli.core.token_exchange import (
|
|
22
22
|
auth_error,
|
|
23
|
-
get_apikey_header,
|
|
24
23
|
parse_and_save_token_response,
|
|
25
|
-
|
|
24
|
+
register_oauth_client,
|
|
26
25
|
)
|
|
27
26
|
|
|
28
27
|
_ASSETS = importlib.resources.files("minitest_cli.assets")
|
|
@@ -59,31 +58,24 @@ def refresh_token(settings: Settings, creds: Credentials) -> Credentials | None:
|
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
def oauth_pkce_login(settings: Settings) -> Credentials:
|
|
62
|
-
"""Run the full OAuth PKCE login flow.
|
|
61
|
+
"""Run the full OAuth PKCE login flow via Supabase's OAuth2 server.
|
|
63
62
|
|
|
64
63
|
Steps:
|
|
65
|
-
1.
|
|
66
|
-
2.
|
|
67
|
-
3.
|
|
68
|
-
4.
|
|
69
|
-
5.
|
|
70
|
-
6.
|
|
64
|
+
1. Start local callback server
|
|
65
|
+
2. Dynamically register an OAuth2 client with Supabase
|
|
66
|
+
3. Generate PKCE code verifier + challenge
|
|
67
|
+
4. Open browser to Supabase authorize endpoint (shows hosted sign-in page)
|
|
68
|
+
5. Wait for callback with authorization code
|
|
69
|
+
6. Exchange code + verifier for tokens at Supabase token endpoint
|
|
70
|
+
7. Save and return credentials
|
|
71
71
|
"""
|
|
72
|
-
supabase_url =
|
|
72
|
+
supabase_url = settings.supabase_url.rstrip("/")
|
|
73
73
|
|
|
74
74
|
# PKCE challenge: base64url(sha256(verifier)) without padding
|
|
75
75
|
code_verifier = secrets.token_urlsafe(64)
|
|
76
76
|
digest = hashlib.sha256(code_verifier.encode()).digest()
|
|
77
77
|
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
|
|
78
78
|
|
|
79
|
-
# CSRF protection: We do NOT generate our own state parameter.
|
|
80
|
-
# Supabase internally uses 'state' as a FlowState UUID (database key) to track
|
|
81
|
-
# the OAuth flow context (redirect_to, PKCE params, etc.). If we override it,
|
|
82
|
-
# Supabase can't find the FlowState record and falls back to Site URL.
|
|
83
|
-
# We still have CSRF protection via:
|
|
84
|
-
# 1. Supabase's FlowState UUID (validated on callback)
|
|
85
|
-
# 2. PKCE (code_challenge + code_verifier) per OAuth 2.1
|
|
86
|
-
|
|
87
79
|
# Start callback server
|
|
88
80
|
auth_code_holder: dict[str, str | None] = {"code": None, "error": None}
|
|
89
81
|
ready_event = Event()
|
|
@@ -122,17 +114,21 @@ def oauth_pkce_login(settings: Settings) -> Credentials:
|
|
|
122
114
|
port = server.server_address[1]
|
|
123
115
|
redirect_uri = f"http://127.0.0.1:{port}/callback"
|
|
124
116
|
|
|
125
|
-
#
|
|
117
|
+
# Register OAuth client with Supabase (dynamic client registration)
|
|
118
|
+
client_id = register_oauth_client(supabase_url, redirect_uri)
|
|
119
|
+
|
|
120
|
+
# Build authorize URL — Supabase's OAuth2 server shows its hosted sign-in page
|
|
126
121
|
authorize_params = urllib.parse.urlencode(
|
|
127
122
|
{
|
|
128
|
-
"
|
|
123
|
+
"client_id": client_id,
|
|
124
|
+
"redirect_uri": redirect_uri,
|
|
129
125
|
"response_type": "code",
|
|
130
126
|
"code_challenge": code_challenge,
|
|
131
127
|
"code_challenge_method": "S256",
|
|
132
|
-
"
|
|
128
|
+
"scope": "openid email profile",
|
|
133
129
|
}
|
|
134
130
|
)
|
|
135
|
-
authorize_url = f"{supabase_url}/auth/v1/authorize?{authorize_params}"
|
|
131
|
+
authorize_url = f"{supabase_url}/auth/v1/oauth/authorize?{authorize_params}"
|
|
136
132
|
|
|
137
133
|
print("Opening browser for authentication...", file=sys.stderr) # noqa: T201
|
|
138
134
|
print(f"If the browser doesn't open, visit:\n{authorize_url}", file=sys.stderr) # noqa: T201
|
|
@@ -156,16 +152,19 @@ def oauth_pkce_login(settings: Settings) -> Credentials:
|
|
|
156
152
|
if not auth_code:
|
|
157
153
|
auth_error("No authorization code received.")
|
|
158
154
|
|
|
159
|
-
# Exchange code for tokens
|
|
155
|
+
# Exchange code for tokens at Supabase's OAuth2 token endpoint
|
|
160
156
|
assert auth_code is not None # for type narrowing
|
|
161
157
|
try:
|
|
162
158
|
token_response = httpx.post(
|
|
163
|
-
f"{supabase_url}/auth/v1/token
|
|
164
|
-
|
|
165
|
-
"
|
|
159
|
+
f"{supabase_url}/auth/v1/oauth/token",
|
|
160
|
+
data={
|
|
161
|
+
"grant_type": "authorization_code",
|
|
162
|
+
"code": auth_code,
|
|
163
|
+
"redirect_uri": redirect_uri,
|
|
164
|
+
"client_id": client_id,
|
|
166
165
|
"code_verifier": code_verifier,
|
|
167
166
|
},
|
|
168
|
-
headers={"Content-Type": "application/
|
|
167
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
169
168
|
timeout=15.0,
|
|
170
169
|
)
|
|
171
170
|
except httpx.HTTPError as exc:
|
|
@@ -6,6 +6,8 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Any, NoReturn
|
|
8
8
|
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
9
11
|
from minitest_cli.core.config import Settings
|
|
10
12
|
from minitest_cli.core.credentials import Credentials, save_credentials
|
|
11
13
|
|
|
@@ -51,6 +53,42 @@ def parse_and_save_token_response(settings: Settings, data: dict[str, Any]) -> C
|
|
|
51
53
|
return None
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
def register_oauth_client(supabase_url: str, redirect_uri: str) -> str:
|
|
57
|
+
"""Dynamically register an OAuth2 client with Supabase and return the client_id."""
|
|
58
|
+
register_url = f"{supabase_url}/auth/v1/oauth/clients/register"
|
|
59
|
+
try:
|
|
60
|
+
resp = httpx.post(
|
|
61
|
+
register_url,
|
|
62
|
+
json={
|
|
63
|
+
"client_name": "minitest-cli",
|
|
64
|
+
"redirect_uris": [redirect_uri],
|
|
65
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
66
|
+
"response_types": ["code"],
|
|
67
|
+
"token_endpoint_auth_method": "none",
|
|
68
|
+
},
|
|
69
|
+
headers={"Content-Type": "application/json"},
|
|
70
|
+
timeout=15.0,
|
|
71
|
+
)
|
|
72
|
+
except httpx.HTTPError as exc:
|
|
73
|
+
auth_error(f"Failed to register OAuth client: {exc}")
|
|
74
|
+
|
|
75
|
+
if resp.status_code not in (200, 201):
|
|
76
|
+
auth_error(f"OAuth client registration failed: {resp.text}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
data = resp.json()
|
|
80
|
+
except ValueError:
|
|
81
|
+
auth_error(
|
|
82
|
+
f"OAuth client registration returned invalid response "
|
|
83
|
+
f"(HTTP {resp.status_code}): {resp.text}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
client_id: str | None = data.get("client_id")
|
|
87
|
+
if not client_id:
|
|
88
|
+
auth_error("OAuth client registration returned no client_id.")
|
|
89
|
+
return client_id # type: ignore[return-value]
|
|
90
|
+
|
|
91
|
+
|
|
54
92
|
def auth_error(message: str) -> NoReturn:
|
|
55
93
|
"""Print auth error to stderr and exit with code 2."""
|
|
56
94
|
print(f"Error: {message}", file=sys.stderr) # noqa: T201
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|