avr-cli 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.
- avr_cli-0.1.0.dist-info/METADATA +298 -0
- avr_cli-0.1.0.dist-info/RECORD +42 -0
- avr_cli-0.1.0.dist-info/WHEEL +5 -0
- avr_cli-0.1.0.dist-info/entry_points.txt +2 -0
- avr_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- avr_cli-0.1.0.dist-info/top_level.txt +1 -0
- avrea_cli/__init__.py +9 -0
- avrea_cli/_build_info.py +2 -0
- avrea_cli/api_client.py +111 -0
- avrea_cli/assets/logo_mark.svg +25 -0
- avrea_cli/audit.py +153 -0
- avrea_cli/auth.py +457 -0
- avrea_cli/billing.py +614 -0
- avrea_cli/click_ext.py +261 -0
- avrea_cli/commands/__init__.py +1 -0
- avrea_cli/commands/auth_cmd.py +296 -0
- avrea_cli/commands/cache.py +312 -0
- avrea_cli/commands/config_cmd.py +180 -0
- avrea_cli/commands/health.py +76 -0
- avrea_cli/commands/internal.py +18 -0
- avrea_cli/commands/job.py +959 -0
- avrea_cli/commands/log.py +146 -0
- avrea_cli/commands/org.py +404 -0
- avrea_cli/commands/repo.py +152 -0
- avrea_cli/commands/run.py +1424 -0
- avrea_cli/commands/settings.py +271 -0
- avrea_cli/commands/status.py +405 -0
- avrea_cli/commands/workflow.py +883 -0
- avrea_cli/config.py +49 -0
- avrea_cli/display.py +431 -0
- avrea_cli/docs_gen.py +859 -0
- avrea_cli/firewall.py +342 -0
- avrea_cli/helpers.py +218 -0
- avrea_cli/json_output.py +169 -0
- avrea_cli/log_display.py +509 -0
- avrea_cli/main.py +210 -0
- avrea_cli/metrics_display.py +347 -0
- avrea_cli/output.py +101 -0
- avrea_cli/paths.py +45 -0
- avrea_cli/repo_context.py +473 -0
- avrea_cli/transport.py +71 -0
- avrea_cli/version.py +64 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Cache management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from avrea_cli.api_client import ApiClient
|
|
4
|
+
from avrea_cli.click_ext import GhGroup
|
|
5
|
+
from avrea_cli.config import CliConfig
|
|
6
|
+
from avrea_cli.display import get_console_url
|
|
7
|
+
from avrea_cli.display import is_piped
|
|
8
|
+
from avrea_cli.display import open_or_print_url
|
|
9
|
+
from avrea_cli.display import print_piped_header
|
|
10
|
+
from avrea_cli.display import print_piped_row
|
|
11
|
+
from avrea_cli.helpers import ensure_authenticated
|
|
12
|
+
from avrea_cli.helpers import ensure_ctx
|
|
13
|
+
from avrea_cli.helpers import ensure_prompts_allowed
|
|
14
|
+
from avrea_cli.helpers import format_size
|
|
15
|
+
from avrea_cli.helpers import get_org_id
|
|
16
|
+
from avrea_cli.helpers import get_org_slug
|
|
17
|
+
from avrea_cli.helpers import handle_http_error
|
|
18
|
+
from avrea_cli.json_output import emit_json
|
|
19
|
+
from avrea_cli.json_output import emit_json_record
|
|
20
|
+
from avrea_cli.json_output import handle_json_meta
|
|
21
|
+
from avrea_cli.json_output import make_schema
|
|
22
|
+
from avrea_cli.json_output import reject_web_with_json
|
|
23
|
+
from avrea_cli.json_output import split_fields
|
|
24
|
+
from avrea_cli.output import format_key_value
|
|
25
|
+
from avrea_cli.output import format_timestamp
|
|
26
|
+
from avrea_cli.output import output_list
|
|
27
|
+
from avrea_cli.repo_context import resolve_repo_named
|
|
28
|
+
from avrea_cli.repo_context import resolve_repo_or_detect
|
|
29
|
+
from typing import Any
|
|
30
|
+
import click
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group(cls=GhGroup)
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def cache(ctx):
|
|
37
|
+
"""Inspect and manage the Avrea build cache."""
|
|
38
|
+
ensure_ctx(ctx)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_CACHE_LIST_FIELDS = make_schema(
|
|
42
|
+
"cache_type", "key", "ref", "size_bytes", "created_at", "last_accessed_at", "version", "hit_count"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cache.command("list")
|
|
47
|
+
@click.option("--repo", "repo_id", help="Repository (org/repo or rep-xxx). Auto-detected from git remote if omitted.")
|
|
48
|
+
@click.option("--org", "org_id", help="Organization ID. Uses default org if not specified (see: avr config set org).")
|
|
49
|
+
@click.option(
|
|
50
|
+
"--type",
|
|
51
|
+
"cache_type",
|
|
52
|
+
type=str,
|
|
53
|
+
help="Filter by cache type (e.g. gha, bazel, turbo, rclone).",
|
|
54
|
+
)
|
|
55
|
+
@click.option("--key", help="Filter by key prefix.")
|
|
56
|
+
@click.option("--ref", help="Filter by exact ref match.")
|
|
57
|
+
@click.option(
|
|
58
|
+
"-L", "--limit", type=click.IntRange(1, 1000), default=100, show_default=True, help="Max entries to return."
|
|
59
|
+
)
|
|
60
|
+
@click.option("--offset", type=click.IntRange(min=0), default=0, show_default=True, help="Number of entries to skip.")
|
|
61
|
+
@click.option(
|
|
62
|
+
"--order",
|
|
63
|
+
type=click.Choice(["created_at.desc", "created_at.asc"]),
|
|
64
|
+
default="created_at.desc",
|
|
65
|
+
show_default=True,
|
|
66
|
+
help="Sort order.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--json",
|
|
70
|
+
"json_fields",
|
|
71
|
+
default=None,
|
|
72
|
+
help='Output JSON. Pass comma-separated field names, "*" for all fields, or "?" to list available fields.',
|
|
73
|
+
)
|
|
74
|
+
@click.option("-q", "--jq", "jq_expr", default=None, help="Filter --json output through a jq expression.")
|
|
75
|
+
@click.option("--web", is_flag=True, help="Open in browser.")
|
|
76
|
+
@click.pass_context
|
|
77
|
+
def cache_list(ctx, repo_id, org_id, cache_type, key, ref, limit, offset, order, json_fields, jq_expr, web: bool):
|
|
78
|
+
"""List cache entries for a repository.
|
|
79
|
+
|
|
80
|
+
\b
|
|
81
|
+
Examples:
|
|
82
|
+
avr cache list --repo rep-abc123
|
|
83
|
+
avr cache list --repo rep-abc123 --type gha --limit 50
|
|
84
|
+
avr cache list --repo rep-abc123 --key "node_modules" --ref refs/heads/main
|
|
85
|
+
avr cache list --repo rep-abc123 --json key,size_bytes,created_at
|
|
86
|
+
|
|
87
|
+
\b
|
|
88
|
+
JSON FIELDS
|
|
89
|
+
cache_type, created_at, hit_count, key, last_accessed_at, ref, size_bytes, version
|
|
90
|
+
"""
|
|
91
|
+
reject_web_with_json(json_fields, web)
|
|
92
|
+
if handle_json_meta(json_fields, jq_expr, _CACHE_LIST_FIELDS):
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
client: ApiClient = ctx.obj["client"]
|
|
96
|
+
config: CliConfig = ctx.obj["config"]
|
|
97
|
+
ensure_authenticated(config)
|
|
98
|
+
|
|
99
|
+
org_id = get_org_id(config, org_id, client=client)
|
|
100
|
+
repo_id = resolve_repo_or_detect(client, config, org_id, repo_id, required=True)
|
|
101
|
+
|
|
102
|
+
if web:
|
|
103
|
+
slug = get_org_slug(client, org_id)
|
|
104
|
+
console_url = get_console_url(config.public_api_url)
|
|
105
|
+
url = f"{console_url}/org/{slug}/caches/{repo_id}"
|
|
106
|
+
open_or_print_url(url)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
params: dict[str, Any] = {"limit": limit, "order": order}
|
|
110
|
+
if cache_type:
|
|
111
|
+
params["cache_type"] = cache_type.lower()
|
|
112
|
+
if key:
|
|
113
|
+
params["key"] = key
|
|
114
|
+
if ref:
|
|
115
|
+
params["ref"] = ref
|
|
116
|
+
if offset:
|
|
117
|
+
params["offset"] = offset
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
response = client.public_get(f"/orgs/{org_id}/repos/{repo_id}/cache/entries", params=params)
|
|
121
|
+
except httpx.HTTPStatusError as exc:
|
|
122
|
+
handle_http_error(exc, "list cache entries")
|
|
123
|
+
|
|
124
|
+
entries = response["data"]
|
|
125
|
+
|
|
126
|
+
if json_fields is not None:
|
|
127
|
+
emit_json(entries, split_fields(json_fields, _CACHE_LIST_FIELDS), _CACHE_LIST_FIELDS, jq_expr)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if is_piped():
|
|
131
|
+
print_piped_header(["cache_type", "key", "ref", "size_bytes", "created_at"])
|
|
132
|
+
for e in entries:
|
|
133
|
+
print_piped_row(
|
|
134
|
+
[
|
|
135
|
+
e.get("cache_type", ""),
|
|
136
|
+
e.get("key", ""),
|
|
137
|
+
e.get("ref", ""),
|
|
138
|
+
e.get("size_bytes", 0),
|
|
139
|
+
e.get("created_at", ""),
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
for entry in entries:
|
|
145
|
+
entry["size_display"] = format_size(entry.get("size_bytes", 0))
|
|
146
|
+
entry["key_display"] = entry.get("key") or "-"
|
|
147
|
+
entry["ref_display"] = entry.get("ref") or "-"
|
|
148
|
+
entry["created_display"] = format_timestamp(entry.get("created_at"))
|
|
149
|
+
|
|
150
|
+
output_list(
|
|
151
|
+
entries,
|
|
152
|
+
columns=["cache_type", "key_display", "ref_display", "size_display", "created_display"],
|
|
153
|
+
column_labels=["Type", "Key", "Ref", "Size", "Created"],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
total = response.get("total", len(entries))
|
|
157
|
+
shown = offset + len(entries)
|
|
158
|
+
if shown < total:
|
|
159
|
+
click.echo(f"\nShowing {shown}/{total}. Next page: --offset {shown}", err=True)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_CACHE_USAGE_FIELDS = make_schema(
|
|
163
|
+
total_size_bytes="data.total_size_bytes",
|
|
164
|
+
quota_bytes="data.quota_bytes",
|
|
165
|
+
over_quota="data.over_quota",
|
|
166
|
+
by_type="data.by_type",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@cache.command("usage")
|
|
171
|
+
@click.option("--repo", "repo_id", help="Repository (org/repo or rep-xxx). Auto-detected from git remote if omitted.")
|
|
172
|
+
@click.option("--org", "org_id", help="Organization ID. Uses default org if not specified (see: avr config set org).")
|
|
173
|
+
@click.option(
|
|
174
|
+
"--json",
|
|
175
|
+
"json_fields",
|
|
176
|
+
default=None,
|
|
177
|
+
help='Output JSON. Pass comma-separated field names, "*" for all fields, or "?" to list available fields.',
|
|
178
|
+
)
|
|
179
|
+
@click.option("-q", "--jq", "jq_expr", default=None, help="Filter --json output through a jq expression.")
|
|
180
|
+
@click.pass_context
|
|
181
|
+
def cache_usage(ctx, repo_id, org_id, json_fields, jq_expr):
|
|
182
|
+
"""Show cache usage summary for a repository.
|
|
183
|
+
|
|
184
|
+
\b
|
|
185
|
+
Examples:
|
|
186
|
+
avr cache usage --repo rep-abc123
|
|
187
|
+
avr cache usage --repo rep-abc123 --json '*'
|
|
188
|
+
|
|
189
|
+
\b
|
|
190
|
+
JSON FIELDS
|
|
191
|
+
by_type, over_quota, quota_bytes, total_size_bytes
|
|
192
|
+
"""
|
|
193
|
+
if handle_json_meta(json_fields, jq_expr, _CACHE_USAGE_FIELDS):
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
client: ApiClient = ctx.obj["client"]
|
|
197
|
+
config: CliConfig = ctx.obj["config"]
|
|
198
|
+
ensure_authenticated(config)
|
|
199
|
+
|
|
200
|
+
org_id = get_org_id(config, org_id, client=client)
|
|
201
|
+
repo_id = resolve_repo_or_detect(client, config, org_id, repo_id, required=True)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
response = client.public_get(f"/orgs/{org_id}/repos/{repo_id}/cache")
|
|
205
|
+
except httpx.HTTPStatusError as exc:
|
|
206
|
+
handle_http_error(exc, "get cache usage")
|
|
207
|
+
|
|
208
|
+
if json_fields is not None:
|
|
209
|
+
emit_json_record(response, split_fields(json_fields, _CACHE_USAGE_FIELDS), _CACHE_USAGE_FIELDS, jq_expr)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
data = response["data"]
|
|
213
|
+
total = data.get("total_size_bytes", 0)
|
|
214
|
+
quota = data.get("quota_bytes", 0)
|
|
215
|
+
pct = (total / quota * 100) if quota > 0 else 0
|
|
216
|
+
|
|
217
|
+
click.echo(
|
|
218
|
+
format_key_value(
|
|
219
|
+
{
|
|
220
|
+
"Total Size": format_size(total),
|
|
221
|
+
"Quota": format_size(quota),
|
|
222
|
+
"Usage": f"{pct:.1f}%",
|
|
223
|
+
"Over Quota": "yes" if data.get("over_quota") else "no",
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
by_type = data.get("by_type", [])
|
|
229
|
+
if by_type:
|
|
230
|
+
click.echo()
|
|
231
|
+
for bt in by_type:
|
|
232
|
+
bt["size_display"] = format_size(bt.get("size_bytes", 0))
|
|
233
|
+
|
|
234
|
+
output_list(
|
|
235
|
+
by_type,
|
|
236
|
+
columns=["cache_type", "size_display", "entry_count"],
|
|
237
|
+
column_labels=["Type", "Size", "Count"],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@cache.command("delete")
|
|
242
|
+
@click.option("--repo", "repo_id", help="Repository (org/repo or rep-xxx). Auto-detected from git remote if omitted.")
|
|
243
|
+
@click.option("--org", "org_id", help="Organization ID. Uses default org if not specified (see: avr config set org).")
|
|
244
|
+
@click.option(
|
|
245
|
+
"--type",
|
|
246
|
+
"cache_type",
|
|
247
|
+
type=str,
|
|
248
|
+
default=None,
|
|
249
|
+
help="Cache type (required with --key, e.g. gha, bazel, sccache).",
|
|
250
|
+
)
|
|
251
|
+
@click.option("--key", default=None, help="Delete entries matching this cache key name.")
|
|
252
|
+
@click.option("--ref", default=None, help="Ref to narrow deletion scope (used by gha).")
|
|
253
|
+
@click.option("--all", "delete_all", is_flag=True, help="Delete ALL cache entries for the repository.")
|
|
254
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
|
|
255
|
+
@click.pass_context
|
|
256
|
+
def cache_delete(ctx, repo_id, org_id, cache_type, key, ref, delete_all, yes):
|
|
257
|
+
"""Delete cache entries by key name or all entries.
|
|
258
|
+
|
|
259
|
+
Exactly one of --key or --all must be provided.
|
|
260
|
+
When using --key, --type is required to scope the deletion.
|
|
261
|
+
|
|
262
|
+
\b
|
|
263
|
+
Examples:
|
|
264
|
+
avr cache delete --repo rep-abc123 --type gha --key "node_modules" --ref refs/heads/main --yes
|
|
265
|
+
avr cache delete --repo rep-abc123 --all --yes
|
|
266
|
+
"""
|
|
267
|
+
client: ApiClient = ctx.obj["client"]
|
|
268
|
+
config: CliConfig = ctx.obj["config"]
|
|
269
|
+
ensure_authenticated(config)
|
|
270
|
+
|
|
271
|
+
provided = sum(bool(x) for x in (key, delete_all))
|
|
272
|
+
if provided != 1:
|
|
273
|
+
click.echo("Error: Provide exactly one of --key or --all.", err=True)
|
|
274
|
+
raise click.Abort()
|
|
275
|
+
|
|
276
|
+
if key and not cache_type:
|
|
277
|
+
click.echo("Error: --type is required when using --key.", err=True)
|
|
278
|
+
raise click.Abort()
|
|
279
|
+
|
|
280
|
+
org_id = get_org_id(config, org_id, client=client)
|
|
281
|
+
repo_id, repo_label = resolve_repo_named(client, config, org_id, repo_id)
|
|
282
|
+
|
|
283
|
+
if key:
|
|
284
|
+
if not yes:
|
|
285
|
+
ensure_prompts_allowed("cache delete needs confirmation")
|
|
286
|
+
ref_scope = f"ref '{ref}'" if ref else "across all refs"
|
|
287
|
+
click.confirm(
|
|
288
|
+
f"Delete entries in {repo_label} matching type '{cache_type}', key '{key}' ({ref_scope})?",
|
|
289
|
+
abort=True,
|
|
290
|
+
)
|
|
291
|
+
try:
|
|
292
|
+
params: dict[str, str] = {"cache_type": cache_type, "key": key}
|
|
293
|
+
if ref:
|
|
294
|
+
params["ref"] = ref
|
|
295
|
+
response = client.public_delete(
|
|
296
|
+
f"/orgs/{org_id}/repos/{repo_id}/cache/entries",
|
|
297
|
+
params=params,
|
|
298
|
+
)
|
|
299
|
+
count = response.get("deleted_count", 0) if response else 0
|
|
300
|
+
click.echo(f"Deleted {count} cache {'entry' if count == 1 else 'entries'}.")
|
|
301
|
+
except httpx.HTTPStatusError as exc:
|
|
302
|
+
handle_http_error(exc, "delete cache entries")
|
|
303
|
+
else:
|
|
304
|
+
if not yes:
|
|
305
|
+
ensure_prompts_allowed("cache purge needs confirmation")
|
|
306
|
+
click.confirm(f"Delete ALL cache entries for {repo_label}?", abort=True)
|
|
307
|
+
try:
|
|
308
|
+
response = client.public_delete(f"/orgs/{org_id}/repos/{repo_id}/cache")
|
|
309
|
+
count = response.get("deleted_count", 0) if response else 0
|
|
310
|
+
click.echo(f"Purged {count} cache {'entry' if count == 1 else 'entries'} from {repo_label}.")
|
|
311
|
+
except httpx.HTTPStatusError as exc:
|
|
312
|
+
handle_http_error(exc, "purge cache")
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""CLI configuration commands (avr config)."""
|
|
2
|
+
|
|
3
|
+
from avrea_cli import auth
|
|
4
|
+
from avrea_cli.api_client import ApiClient
|
|
5
|
+
from avrea_cli.click_ext import GhGroup
|
|
6
|
+
from avrea_cli.config import CliConfig
|
|
7
|
+
from avrea_cli.helpers import ensure_authenticated
|
|
8
|
+
from avrea_cli.helpers import ensure_ctx
|
|
9
|
+
from avrea_cli.helpers import get_org_slug
|
|
10
|
+
from avrea_cli.helpers import handle_http_error
|
|
11
|
+
from avrea_cli.repo_context import detect_repo_from_git
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
import click
|
|
14
|
+
import httpx
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fetch_me(client: ApiClient) -> dict | None:
|
|
19
|
+
"""Best-effort fetch of /users/me; returns None on any error."""
|
|
20
|
+
try:
|
|
21
|
+
response = client.public_get("/users/me")
|
|
22
|
+
return response.get("data") or response
|
|
23
|
+
except httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _src(label: str) -> str:
|
|
28
|
+
return click.style(f"(from {label})", dim=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _print_config_status(ctx) -> None:
|
|
32
|
+
cfg: CliConfig = ctx.obj["config"]
|
|
33
|
+
client: ApiClient = ctx.obj["client"]
|
|
34
|
+
|
|
35
|
+
# Header: API host with source annotation
|
|
36
|
+
host = urlparse(cfg.public_api_url).hostname or cfg.public_api_url
|
|
37
|
+
if os.getenv("AVR_HOST"):
|
|
38
|
+
host_src = _src("AVR_HOST")
|
|
39
|
+
elif auth.load_default_host():
|
|
40
|
+
host_src = _src("hosts.json")
|
|
41
|
+
else:
|
|
42
|
+
host_src = _src("default")
|
|
43
|
+
click.echo(f"{click.style(host, bold=True)} {host_src}")
|
|
44
|
+
|
|
45
|
+
# Auth status line
|
|
46
|
+
if cfg.auth_token:
|
|
47
|
+
me = _fetch_me(client)
|
|
48
|
+
token_src = _src("AVR_TOKEN" if os.getenv("AVR_TOKEN") else "hosts.json")
|
|
49
|
+
if me and me.get("email"):
|
|
50
|
+
click.echo(f" {click.style('✓', fg='green')} authenticated as {me['email']} {token_src}")
|
|
51
|
+
elif me:
|
|
52
|
+
click.echo(f" {click.style('✓', fg='green')} authenticated {token_src}")
|
|
53
|
+
else:
|
|
54
|
+
click.echo(f" {click.style('!', fg='yellow')} token set but /users/me failed {token_src}")
|
|
55
|
+
else:
|
|
56
|
+
click.echo(f" {click.style('✗', fg='red')} not authenticated (run: avr auth login)")
|
|
57
|
+
|
|
58
|
+
# Active org — show slug if we can resolve it
|
|
59
|
+
if cfg.default_org:
|
|
60
|
+
if cfg.auth_token:
|
|
61
|
+
slug = get_org_slug(client, cfg.default_org)
|
|
62
|
+
if slug != cfg.default_org:
|
|
63
|
+
active_org = f"{click.style(slug, bold=True)} ({cfg.default_org})"
|
|
64
|
+
else:
|
|
65
|
+
active_org = click.style(cfg.default_org, bold=True)
|
|
66
|
+
else:
|
|
67
|
+
active_org = click.style(cfg.default_org, bold=True)
|
|
68
|
+
org_src = _src("AVR_ORG" if os.getenv("AVR_ORG") else "hosts.json")
|
|
69
|
+
active_org = f"{active_org} {org_src}"
|
|
70
|
+
else:
|
|
71
|
+
active_org = click.style("(not set)", dim=True)
|
|
72
|
+
|
|
73
|
+
# Default repo — explicit override or auto-detected from git remote
|
|
74
|
+
if cfg.repo_override:
|
|
75
|
+
default_repo = f"{click.style(cfg.repo_override, bold=True)} {_src('AVR_REPO')}"
|
|
76
|
+
else:
|
|
77
|
+
detected = detect_repo_from_git()
|
|
78
|
+
if detected:
|
|
79
|
+
default_repo = f"{click.style(detected, bold=True)} {_src('git remote')}"
|
|
80
|
+
else:
|
|
81
|
+
default_repo = click.style("(no git remote; pass --repo explicitly)", dim=True)
|
|
82
|
+
|
|
83
|
+
click.echo(f" - Active org: {active_org}")
|
|
84
|
+
click.echo(f" - Default repo: {default_repo}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@click.group(cls=GhGroup, invoke_without_command=True)
|
|
88
|
+
@click.pass_context
|
|
89
|
+
def config(ctx):
|
|
90
|
+
"""View and manage CLI configuration."""
|
|
91
|
+
ensure_ctx(ctx)
|
|
92
|
+
if ctx.invoked_subcommand is None:
|
|
93
|
+
_print_config_status(ctx)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@config.command("set")
|
|
97
|
+
@click.argument("key", type=click.Choice(["org"]))
|
|
98
|
+
@click.argument("value")
|
|
99
|
+
@click.pass_context
|
|
100
|
+
def config_set(ctx, key: str, value: str):
|
|
101
|
+
"""Set a CLI configuration value.
|
|
102
|
+
|
|
103
|
+
\b
|
|
104
|
+
Available keys:
|
|
105
|
+
org Active organization ID
|
|
106
|
+
|
|
107
|
+
\b
|
|
108
|
+
Examples:
|
|
109
|
+
avr config set org org-abc123
|
|
110
|
+
"""
|
|
111
|
+
client: ApiClient = ctx.obj["client"]
|
|
112
|
+
cfg: CliConfig = ctx.obj["config"]
|
|
113
|
+
ensure_authenticated(cfg)
|
|
114
|
+
|
|
115
|
+
if key == "org":
|
|
116
|
+
# Verify the org exists and user has access
|
|
117
|
+
try:
|
|
118
|
+
response = client.public_get("/users/me/organizations")
|
|
119
|
+
organizations = response["data"]
|
|
120
|
+
except httpx.HTTPStatusError as exc:
|
|
121
|
+
handle_http_error(exc, "fetch organizations")
|
|
122
|
+
|
|
123
|
+
org_ids = [o["organization_id"] for o in organizations]
|
|
124
|
+
if value not in org_ids:
|
|
125
|
+
click.echo(f"Error: Organization '{value}' not found or you don't have access.", err=True)
|
|
126
|
+
click.echo("Available organizations:", err=True)
|
|
127
|
+
for o in organizations:
|
|
128
|
+
click.echo(f" {o['organization_id']} ({o['name']})", err=True)
|
|
129
|
+
raise click.Abort()
|
|
130
|
+
|
|
131
|
+
auth.store_default_org(value, host=cfg.public_api_url)
|
|
132
|
+
org_name = next((o["name"] for o in organizations if o["organization_id"] == value), value)
|
|
133
|
+
click.echo(f"Default organization set to: {org_name} ({value})")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@config.command("get")
|
|
137
|
+
@click.argument("key", type=click.Choice(["org"]))
|
|
138
|
+
@click.pass_context
|
|
139
|
+
def config_get(ctx, key: str):
|
|
140
|
+
"""Print the value of a configuration key.
|
|
141
|
+
|
|
142
|
+
\b
|
|
143
|
+
Available keys:
|
|
144
|
+
org Active organization ID
|
|
145
|
+
"""
|
|
146
|
+
cfg: CliConfig = ctx.obj["config"]
|
|
147
|
+
values = {"org": cfg.default_org or ""}
|
|
148
|
+
click.echo(values[key])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@config.command("list")
|
|
152
|
+
@click.pass_context
|
|
153
|
+
def config_list(ctx):
|
|
154
|
+
"""Show the active CLI configuration (host, auth, org, default repo)."""
|
|
155
|
+
_print_config_status(ctx)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@config.command("unset")
|
|
159
|
+
@click.argument("key", type=click.Choice(["org"]))
|
|
160
|
+
@click.pass_context
|
|
161
|
+
def config_unset(ctx, key: str):
|
|
162
|
+
"""Remove a configuration override.
|
|
163
|
+
|
|
164
|
+
\b
|
|
165
|
+
Available keys:
|
|
166
|
+
org Drop the stored default organization for the active host
|
|
167
|
+
|
|
168
|
+
\b
|
|
169
|
+
Examples:
|
|
170
|
+
avr config unset org
|
|
171
|
+
"""
|
|
172
|
+
cfg: CliConfig = ctx.obj["config"]
|
|
173
|
+
if key == "org":
|
|
174
|
+
# Match the brevity of `config set`: the host is implied by AVR_HOST
|
|
175
|
+
# / the active default. Showing it would be redundant in the common
|
|
176
|
+
# single-host install.
|
|
177
|
+
if auth.clear_default_org(host=cfg.public_api_url):
|
|
178
|
+
click.echo("Cleared default organization.")
|
|
179
|
+
else:
|
|
180
|
+
click.echo("No default organization was set.")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Health check command."""
|
|
2
|
+
|
|
3
|
+
from avrea_cli.api_client import ApiClient
|
|
4
|
+
from avrea_cli.config import CliConfig
|
|
5
|
+
from avrea_cli.json_output import emit_json_record
|
|
6
|
+
from avrea_cli.json_output import handle_json_meta
|
|
7
|
+
from avrea_cli.json_output import json_options
|
|
8
|
+
from avrea_cli.json_output import make_schema
|
|
9
|
+
from avrea_cli.json_output import split_fields
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
import click
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
# Single-key schema for now; add explicit entries as the endpoint grows new
|
|
15
|
+
# keys (build hash, etc.) so `--json '*'` stays in sync with the typed surface.
|
|
16
|
+
_HEALTH_FIELDS = make_schema("status")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command("health")
|
|
20
|
+
@json_options
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def health(ctx, json_fields, jq_expr):
|
|
23
|
+
"""Check Avrea platform status.
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
Examples:
|
|
27
|
+
avr health
|
|
28
|
+
avr health --json status
|
|
29
|
+
avr health --json '*' -q '.status'
|
|
30
|
+
"""
|
|
31
|
+
if handle_json_meta(json_fields, jq_expr, _HEALTH_FIELDS):
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
client: ApiClient = ctx.obj["client"]
|
|
35
|
+
config: CliConfig = ctx.obj["config"]
|
|
36
|
+
host = urlparse(config.public_api_url).hostname or config.public_api_url
|
|
37
|
+
is_json = json_fields is not None
|
|
38
|
+
|
|
39
|
+
if not is_json:
|
|
40
|
+
click.echo("Checking API health...")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
result = client.public_get("/health")
|
|
44
|
+
except (httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
45
|
+
reason = _humanize_health_failure(exc, host)
|
|
46
|
+
if is_json:
|
|
47
|
+
# Same JSON shape as success — schema-projected, no extra keys.
|
|
48
|
+
# Humans get the friendly reason on stderr; consumers get the
|
|
49
|
+
# stable {"status": "unreachable"} record.
|
|
50
|
+
click.echo(f"API unreachable: {reason}", err=True)
|
|
51
|
+
emit_json_record(
|
|
52
|
+
{"status": "unreachable"},
|
|
53
|
+
split_fields(json_fields, _HEALTH_FIELDS),
|
|
54
|
+
_HEALTH_FIELDS,
|
|
55
|
+
jq_expr,
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
click.echo(f"✗ API: {reason}", err=True)
|
|
59
|
+
raise click.Abort() from None
|
|
60
|
+
|
|
61
|
+
if is_json:
|
|
62
|
+
emit_json_record(result, split_fields(json_fields, _HEALTH_FIELDS), _HEALTH_FIELDS, jq_expr)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
status = result.get("status", "unknown")
|
|
66
|
+
click.echo(f"✓ API: {status}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _humanize_health_failure(exc: Exception, host: str) -> str:
|
|
70
|
+
if isinstance(exc, httpx.ConnectError):
|
|
71
|
+
return f"couldn't reach {host} — check your network connection."
|
|
72
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
73
|
+
return f"{host} took too long to respond."
|
|
74
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
75
|
+
return f"HTTP {exc.response.status_code} from {host}."
|
|
76
|
+
return f"{host} unreachable."
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Hidden ``avr internal`` command group.
|
|
2
|
+
|
|
3
|
+
Houses maintenance commands that ship with the CLI but aren't part of the
|
|
4
|
+
public surface — listed here so the published ``avr`` binary can dogfood
|
|
5
|
+
its own docs generator (``avr internal docs``) without polluting the
|
|
6
|
+
top-level help text.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from avrea_cli.docs_gen import docs_command
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group("internal", hidden=True)
|
|
14
|
+
def internal() -> None:
|
|
15
|
+
"""Maintenance commands (hidden)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
internal.add_command(docs_command)
|