clue2app-cli 0.2.7__tar.gz → 0.3.1__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.
Files changed (39) hide show
  1. {clue2app_cli-0.2.7/src/clue2app_cli.egg-info → clue2app_cli-0.3.1}/PKG-INFO +27 -26
  2. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/README.md +23 -20
  3. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/pyproject.toml +10 -1
  4. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/creds_module.py +87 -10
  5. clue2app_cli-0.3.1/src/clue2app_cli/directory_cmd.py +1076 -0
  6. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/main.py +434 -39
  7. clue2app_cli-0.3.1/src/clue2app_cli/mcp_cmd.py +467 -0
  8. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/sdk.py +968 -23
  9. clue2app_cli-0.3.1/src/clue2app_cli/tests_cmd.py +845 -0
  10. clue2app_cli-0.3.1/src/clue2app_cli/tests_schema/__init__.py +15 -0
  11. clue2app_cli-0.3.1/src/clue2app_cli/tests_schema/c2a-tests.v1.json +255 -0
  12. clue2app_cli-0.3.1/src/clue2app_cli/tests_schema/validator.py +141 -0
  13. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1/src/clue2app_cli.egg-info}/PKG-INFO +27 -26
  14. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/SOURCES.txt +6 -2
  15. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/requires.txt +5 -0
  16. clue2app_cli-0.2.7/setup.py +0 -64
  17. clue2app_cli-0.2.7/src/clue2app_cli.egg-info/not-zip-safe +0 -1
  18. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/LICENSE +0 -0
  19. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/setup.cfg +0 -0
  20. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/__init__.py +0 -0
  21. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/agents/__init__.py +0 -0
  22. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/agents/ecr_healer.py +0 -0
  23. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/auth.py +0 -0
  24. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/config.py +0 -0
  25. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/core_client.py +0 -0
  26. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/curses_wizard.py +0 -0
  27. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/entrypoint_check.py +0 -0
  28. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/__init__.py +0 -0
  29. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/curses_app.py +0 -0
  30. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/data.py +0 -0
  31. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/screens/__init__.py +0 -0
  32. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/textual_app.py +0 -0
  33. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/widgets/__init__.py +0 -0
  34. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui_wizard.py +0 -0
  35. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/version.py +0 -0
  36. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/wizard.py +0 -0
  37. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/dependency_links.txt +0 -0
  38. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/entry_points.txt +0 -0
  39. {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/top_level.txt +0 -0
@@ -1,9 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clue2app-cli
3
- Version: 0.2.7
3
+ Version: 0.3.1
4
4
  Summary: CLI application for clue2app using the clue2app-python-sdk
5
- Home-page: https://github.com/clue2solve/clue2app-cli
6
- Author: Clue2App CLI Team
7
5
  Author-email: Clue2App CLI Team <support@clue2app.com>
8
6
  License: MIT
9
7
  Project-URL: Homepage, https://github.com/clue2solve/clue2app-cli
@@ -33,6 +31,9 @@ Requires-Dist: pydantic>=1.10.0
33
31
  Requires-Dist: toml>=0.10.0
34
32
  Requires-Dist: questionary>=2.0.0
35
33
  Requires-Dist: textual>=0.40.0
34
+ Requires-Dist: jsonschema>=4.18.0
35
+ Requires-Dist: PyYAML>=6.0
36
+ Requires-Dist: clue2app-mcp>=0.1.1; python_version >= "3.10"
36
37
  Provides-Extra: dev
37
38
  Requires-Dist: pytest>=7.0.0; extra == "dev"
38
39
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -45,10 +46,7 @@ Provides-Extra: build
45
46
  Requires-Dist: pyinstaller>=6.0.0; extra == "build"
46
47
  Requires-Dist: invoke>=2.0.0; extra == "build"
47
48
  Requires-Dist: tox>=4.0.0; extra == "build"
48
- Dynamic: author
49
- Dynamic: home-page
50
49
  Dynamic: license-file
51
- Dynamic: requires-python
52
50
 
53
51
  # Clue2App CLI
54
52
 
@@ -59,31 +57,13 @@ Dynamic: requires-python
59
57
  [![Tests](https://img.shields.io/github/actions/workflow/status/clue2solve/clue2app-cli/tests.yml?label=tests)](https://github.com/clue2solve/clue2app-cli/actions/workflows/tests.yml)
60
58
  [![Release](https://img.shields.io/github/v/release/clue2solve/clue2app-cli)](https://github.com/clue2solve/clue2app-cli/releases)
61
59
 
62
- ## Installation
63
-
64
- ### Option 1: pip (Recommended)
60
+ ## Install
65
61
 
66
62
  ```bash
67
63
  pip install clue2app-cli
68
64
  ```
69
65
 
70
- ### Option 2: Standalone Executable
71
-
72
- Download from [Releases](https://github.com/clue2solve/clue2app-cli/releases):
73
-
74
- ```bash
75
- # macOS (Apple Silicon)
76
- curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-arm64 -o c2a
77
- chmod +x c2a && sudo mv c2a /usr/local/bin/
78
-
79
- # macOS (Intel)
80
- curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-x64 -o c2a
81
- chmod +x c2a && sudo mv c2a /usr/local/bin/
82
-
83
- # Linux
84
- curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-linux-x64 -o c2a
85
- chmod +x c2a && sudo mv c2a /usr/local/bin/
86
- ```
66
+ Requires Python 3.10+. Verify with `c2a --version`.
87
67
 
88
68
  ## Quick Start
89
69
 
@@ -128,9 +108,30 @@ c2a rebuild my-app
128
108
  | `c2a rebuild <app>` | Trigger app rebuild |
129
109
  | `c2a env set <app> KEY=value` | Set environment variables |
130
110
  | `c2a secrets create -n <name> -u <user> -p <token>` | Create a git credential secret |
111
+ | `c2a user invite -e <email> -A <account_uuid> -P <project_uuid> -r <role>` | Invite a project member via the invitations flow (SCK-625) |
131
112
  | `c2a llm list` / `c2a llm create` / `c2a llm bind <app> <secret>` | Manage LLM Gateway bindings (see [Daari LLM](docs/daari-llm.md)) |
132
113
  | `c2a daari secrets list` / `bind <app> <secret>` | Manage Daari service bindings (see [Daari](docs/daari.md)) |
133
114
 
115
+ ### Inviting a project member (SCK-625)
116
+
117
+ Pass `-A <account_uuid>` to route through the invitations service
118
+ (`POST /api/v1/invitations/project-member`, backed by SCK-622). The
119
+ new endpoint mints an invitation row, sends the redemption email by
120
+ default, and returns a redemption URL the CLI prints:
121
+
122
+ ```bash
123
+ # Server sends the invitation email (default delivery).
124
+ c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r CONTRIBUTOR
125
+
126
+ # --copy-only skips the email; hand-deliver the printed URL yourself.
127
+ c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r VIEWER --copy-only
128
+ ```
129
+
130
+ `-r` accepts `VIEWER`, `CONTRIBUTOR`, or `PROJECT_ADMIN`. Without
131
+ `-A`, `c2a user invite` keeps its legacy behaviour (invite-with-
132
+ project-grants via `/api/accounts/{id}/members`, or the LOCAL user
133
+ create flow with `-u`/`-p`).
134
+
134
135
  ## Configuration
135
136
 
136
137
  Config files are stored in `~/.c2a/`:
@@ -7,31 +7,13 @@
7
7
  [![Tests](https://img.shields.io/github/actions/workflow/status/clue2solve/clue2app-cli/tests.yml?label=tests)](https://github.com/clue2solve/clue2app-cli/actions/workflows/tests.yml)
8
8
  [![Release](https://img.shields.io/github/v/release/clue2solve/clue2app-cli)](https://github.com/clue2solve/clue2app-cli/releases)
9
9
 
10
- ## Installation
11
-
12
- ### Option 1: pip (Recommended)
10
+ ## Install
13
11
 
14
12
  ```bash
15
13
  pip install clue2app-cli
16
14
  ```
17
15
 
18
- ### Option 2: Standalone Executable
19
-
20
- Download from [Releases](https://github.com/clue2solve/clue2app-cli/releases):
21
-
22
- ```bash
23
- # macOS (Apple Silicon)
24
- curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-arm64 -o c2a
25
- chmod +x c2a && sudo mv c2a /usr/local/bin/
26
-
27
- # macOS (Intel)
28
- curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-x64 -o c2a
29
- chmod +x c2a && sudo mv c2a /usr/local/bin/
30
-
31
- # Linux
32
- curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-linux-x64 -o c2a
33
- chmod +x c2a && sudo mv c2a /usr/local/bin/
34
- ```
16
+ Requires Python 3.10+. Verify with `c2a --version`.
35
17
 
36
18
  ## Quick Start
37
19
 
@@ -76,9 +58,30 @@ c2a rebuild my-app
76
58
  | `c2a rebuild <app>` | Trigger app rebuild |
77
59
  | `c2a env set <app> KEY=value` | Set environment variables |
78
60
  | `c2a secrets create -n <name> -u <user> -p <token>` | Create a git credential secret |
61
+ | `c2a user invite -e <email> -A <account_uuid> -P <project_uuid> -r <role>` | Invite a project member via the invitations flow (SCK-625) |
79
62
  | `c2a llm list` / `c2a llm create` / `c2a llm bind <app> <secret>` | Manage LLM Gateway bindings (see [Daari LLM](docs/daari-llm.md)) |
80
63
  | `c2a daari secrets list` / `bind <app> <secret>` | Manage Daari service bindings (see [Daari](docs/daari.md)) |
81
64
 
65
+ ### Inviting a project member (SCK-625)
66
+
67
+ Pass `-A <account_uuid>` to route through the invitations service
68
+ (`POST /api/v1/invitations/project-member`, backed by SCK-622). The
69
+ new endpoint mints an invitation row, sends the redemption email by
70
+ default, and returns a redemption URL the CLI prints:
71
+
72
+ ```bash
73
+ # Server sends the invitation email (default delivery).
74
+ c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r CONTRIBUTOR
75
+
76
+ # --copy-only skips the email; hand-deliver the printed URL yourself.
77
+ c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r VIEWER --copy-only
78
+ ```
79
+
80
+ `-r` accepts `VIEWER`, `CONTRIBUTOR`, or `PROJECT_ADMIN`. Without
81
+ `-A`, `c2a user invite` keeps its legacy behaviour (invite-with-
82
+ project-grants via `/api/accounts/{id}/members`, or the LOCAL user
83
+ create flow with `-u`/`-p`).
84
+
82
85
  ## Configuration
83
86
 
84
87
  Config files are stored in `~/.c2a/`:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clue2app-cli"
7
- version = "0.2.7"
7
+ version = "0.3.1"
8
8
  description = "CLI application for clue2app using the clue2app-python-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -36,6 +36,9 @@ dependencies = [
36
36
  "toml>=0.10.0",
37
37
  "questionary>=2.0.0", # Interactive prompts for wizard
38
38
  "textual>=0.40.0", # Modern TUI framework with CSS animations
39
+ "jsonschema>=4.18.0", # .c2a-tests.yaml validator (SCK-463); needs Draft 2020-12 support
40
+ "PyYAML>=6.0", # Parse .c2a-tests.yaml for `c2a tests validate`
41
+ "clue2app-mcp>=0.1.1; python_version>='3.10'", # Bundle MCP server transitively (v0.3.0); MCP requires Python 3.10+
39
42
  ]
40
43
 
41
44
  [project.optional-dependencies]
@@ -70,6 +73,12 @@ where = ["src"]
70
73
  [tool.setuptools.package-dir]
71
74
  "" = "src"
72
75
 
76
+ # Ship the bundled .c2a-tests.yaml v1 schema (SCK-463). `c2a tests validate`
77
+ # needs it at runtime; without an explicit package-data line, sdists ship it
78
+ # but wheels can drop it depending on setuptools version. Belt-and-braces.
79
+ [tool.setuptools.package-data]
80
+ "clue2app_cli.tests_schema" = ["*.json"]
81
+
73
82
  [tool.black]
74
83
  line-length = 88
75
84
  target-version = ['py38']
@@ -109,23 +109,51 @@ def trigger_kpack_rebuild(image_name: str, namespace: str) -> tuple[bool, str]:
109
109
  class CredStatus:
110
110
  """Result of probing one git secret's PAT against GitHub.
111
111
 
112
- `alive`: bool — token authenticated successfully against GitHub.
113
- `reason`: short tag for grouping (e.g. "ok", "401-unauthorized",
114
- "saml-required", "no-password", "unreachable").
115
- `detail`: human-readable extra info (which scope was missing, etc.).
112
+ `alive`: token authenticated successfully against GitHub.
113
+ `reason`: short tag for grouping (e.g. "ok", "401-unauthorized",
114
+ "saml-required", "no-password", "unreachable",
115
+ "expiring-soon").
116
+ `detail`: human-readable extra info (which scope was missing, etc.).
117
+ `expires_at`: ISO-8601 expiry timestamp from GitHub's
118
+ `github-authentication-token-expiration` response header,
119
+ if present. Returned by GitHub for classic + fine-grained
120
+ PATs (`ghp_*` / `github_pat_*`). Absent for `gho_*` OAuth
121
+ tokens (incl. `gh auth token`) and GitHub App installation
122
+ tokens — those return None and the caller can't predict
123
+ expiry.
124
+ `days_to_expiry`: integer days from now until `expires_at`. Negative
125
+ means already expired. None when `expires_at` is None.
116
126
  """
117
127
 
118
128
  alive: bool
119
129
  reason: str
120
130
  detail: str = ""
131
+ expires_at: Optional[str] = None
132
+ days_to_expiry: Optional[int] = None
133
+
134
+
135
+ # Tokens within this many days of expiry get the `expiring-soon` reason
136
+ # even though they're still alive. Picked at 14d so a weekly check cycle
137
+ # always catches them at least twice before they break.
138
+ EXPIRY_WARN_DAYS = 14
121
139
 
122
140
 
123
141
  def probe_github_pat(token: str, *, timeout_s: float = 10.0) -> CredStatus:
124
- """Hit `GET https://api.github.com/user` with the token. 200 means the
125
- token is valid AND has at least basic identity scope. 401 means
126
- expired/revoked. 403 with `X-GitHub-SSO` header means the org needs
127
- SAML SSO authorization for this token (the most common 'silent break'
128
- in clue2solve's setup)."""
142
+ """Hit `GET https://api.github.com/user` with the token. Classifies
143
+ the response:
144
+
145
+ - 200 + no expiry header -> alive, reason="ok"
146
+ - 200 + expiry header > 14d -> alive, reason="ok", expires_at set
147
+ - 200 + expiry header within 14d -> alive, reason="expiring-soon"
148
+ - 401 -> dead, reason="401-unauthorized"
149
+ - 403 + X-GitHub-SSO header -> dead, reason="saml-required"
150
+ - 403 generic -> dead, reason="403-forbidden"
151
+ - URLError / timeout -> unknown, reason="unreachable"
152
+
153
+ Expiry header (`github-authentication-token-expiration`) is only
154
+ populated for classic + fine-grained PATs. `gho_*` OAuth tokens
155
+ don't expose it; we can only detect them after they break.
156
+ """
129
157
  import urllib.error
130
158
  import urllib.request
131
159
 
@@ -143,7 +171,16 @@ def probe_github_pat(token: str, *, timeout_s: float = 10.0) -> CredStatus:
143
171
  try:
144
172
  with urllib.request.urlopen(req, timeout=timeout_s) as r:
145
173
  if r.status == 200:
146
- return CredStatus(True, "ok")
174
+ exp_raw = r.headers.get("github-authentication-token-expiration")
175
+ expires_at, days = _parse_expiry_header(exp_raw)
176
+ if days is not None and days <= EXPIRY_WARN_DAYS:
177
+ return CredStatus(
178
+ True, "expiring-soon",
179
+ f"expires in {days} day(s)",
180
+ expires_at=expires_at,
181
+ days_to_expiry=days,
182
+ )
183
+ return CredStatus(True, "ok", expires_at=expires_at, days_to_expiry=days)
147
184
  return CredStatus(False, f"unexpected-{r.status}", "")
148
185
  except urllib.error.HTTPError as e:
149
186
  if e.code == 401:
@@ -160,6 +197,46 @@ def probe_github_pat(token: str, *, timeout_s: float = 10.0) -> CredStatus:
160
197
  return CredStatus(False, "unreachable", str(e))
161
198
 
162
199
 
200
+ def _parse_expiry_header(raw: Optional[str]) -> tuple[Optional[str], Optional[int]]:
201
+ """Parse GitHub's `github-authentication-token-expiration` header.
202
+
203
+ Format example: `2026-08-15 14:30:00 UTC`. Returns
204
+ (iso_string, days_to_expiry). Both None if header is absent or
205
+ unparseable — we want this to be a soft signal, never a crash."""
206
+ if not raw:
207
+ return None, None
208
+ import datetime
209
+
210
+ raw = raw.strip()
211
+ formats = [
212
+ "%Y-%m-%d %H:%M:%S %Z",
213
+ "%Y-%m-%dT%H:%M:%SZ",
214
+ "%Y-%m-%d %H:%M:%S UTC",
215
+ ]
216
+ parsed: Optional[datetime.datetime] = None
217
+ for fmt in formats:
218
+ try:
219
+ parsed = datetime.datetime.strptime(raw, fmt)
220
+ break
221
+ except ValueError:
222
+ continue
223
+ if parsed is None:
224
+ # Last resort: try fromisoformat after Z->+00:00
225
+ try:
226
+ parsed = datetime.datetime.fromisoformat(raw.replace("Z", "+00:00"))
227
+ except ValueError:
228
+ return None, None
229
+
230
+ # Make tz-aware in UTC if naive (GitHub header is always UTC)
231
+ if parsed.tzinfo is None:
232
+ parsed = parsed.replace(tzinfo=datetime.timezone.utc)
233
+
234
+ now = datetime.datetime.now(datetime.timezone.utc)
235
+ delta = parsed - now
236
+ days = int(delta.total_seconds() // 86400) # truncate to whole days; can be negative
237
+ return parsed.isoformat(), days
238
+
239
+
163
240
  # ---------------------------------------------------------------------------
164
241
  # Cluster discovery — find all kpack Image -> git secret references
165
242
  # ---------------------------------------------------------------------------