skillnav 0.3.0__tar.gz → 0.4.2__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.
- {skillnav-0.3.0 → skillnav-0.4.2}/PKG-INFO +6 -5
- {skillnav-0.3.0 → skillnav-0.4.2}/README.md +2 -3
- {skillnav-0.3.0 → skillnav-0.4.2}/pyproject.toml +23 -3
- skillnav-0.4.2/src/skillnav/__init__.py +3 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/__main__.py +1 -1
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/api.py +8 -7
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/cli.py +140 -65
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/context.py +2 -1
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/contributors.py +13 -4
- skillnav-0.4.2/src/skillnav/error_hints.py +424 -0
- skillnav-0.4.2/src/skillnav/errors.py +53 -0
- skillnav-0.4.2/src/skillnav/output.py +874 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/self_update.py +4 -2
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav.egg-info/PKG-INFO +6 -5
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav.egg-info/SOURCES.txt +1 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav.egg-info/requires.txt +2 -0
- skillnav-0.3.0/src/skillnav/__init__.py +0 -3
- skillnav-0.3.0/src/skillnav/errors.py +0 -32
- skillnav-0.3.0/src/skillnav/output.py +0 -427
- {skillnav-0.3.0 → skillnav-0.4.2}/setup.cfg +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/config.py +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/packages.py +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/publish_metadata.py +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/skill_categories.py +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav/urls.py +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav.egg-info/dependency_links.txt +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav.egg-info/entry_points.txt +0 -0
- {skillnav-0.3.0 → skillnav-0.4.2}/src/skillnav.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skillnav
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: CLI client for the Skill management platform (
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: CLI client for the Skill management platform (SkillNavigator)
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: skill,cli,agent,registry
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -12,17 +12,19 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
Requires-Dist: typer>=0.12
|
|
13
13
|
Provides-Extra: dev
|
|
14
14
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
15
17
|
|
|
16
18
|
# skillnav
|
|
17
19
|
|
|
18
|
-
Official CLI client for the Skill management platform (
|
|
20
|
+
Official CLI client for the Skill management platform (SkillNavigator).
|
|
19
21
|
|
|
20
22
|
Specification: `docs/cli-design.md` in the platform repository.
|
|
21
23
|
|
|
22
24
|
## Install
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
|
|
27
|
+
pip install skillnav -i https://mirrors.aliyun.com/pypi/simple/
|
|
26
28
|
# or from this repo:
|
|
27
29
|
pip install -e "cli-py[dev]"
|
|
28
30
|
```
|
|
@@ -36,7 +38,6 @@ skillnav config test
|
|
|
36
38
|
skillnav login --api-key sk_...
|
|
37
39
|
skillnav search demo
|
|
38
40
|
skillnav info demo-skill
|
|
39
|
-
skillnav review examples/demo-skill
|
|
40
41
|
skillnav publish examples/demo-skill --dry-run
|
|
41
42
|
skillnav download demo-skill -o /tmp/demo.zip
|
|
42
43
|
skillnav update # upgrade when PyPI has a newer release
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# skillnav
|
|
2
2
|
|
|
3
|
-
Official CLI client for the Skill management platform (
|
|
3
|
+
Official CLI client for the Skill management platform (SkillNavigator).
|
|
4
4
|
|
|
5
5
|
Specification: `docs/cli-design.md` in the platform repository.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
pip install skillnav -i https://mirrors.aliyun.com/pypi/simple/
|
|
11
11
|
# or from this repo:
|
|
12
12
|
pip install -e "cli-py[dev]"
|
|
13
13
|
```
|
|
@@ -21,7 +21,6 @@ skillnav config test
|
|
|
21
21
|
skillnav login --api-key sk_...
|
|
22
22
|
skillnav search demo
|
|
23
23
|
skillnav info demo-skill
|
|
24
|
-
skillnav review examples/demo-skill
|
|
25
24
|
skillnav publish examples/demo-skill --dry-run
|
|
26
25
|
skillnav download demo-skill -o /tmp/demo.zip
|
|
27
26
|
skillnav update # upgrade when PyPI has a newer release
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "skillnav"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "CLI client for the Skill management platform (
|
|
7
|
+
version = "0.4.2"
|
|
8
|
+
description = "CLI client for the Skill management platform (SkillNavigator)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = { text = "Apache-2.0" }
|
|
@@ -20,10 +20,30 @@ dependencies = [
|
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[project.optional-dependencies]
|
|
23
|
-
dev = [
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.0",
|
|
25
|
+
"ruff>=0.9",
|
|
26
|
+
"mypy>=1.10",
|
|
27
|
+
]
|
|
24
28
|
|
|
25
29
|
[project.scripts]
|
|
26
30
|
skillnav = "skillnav.cli:main"
|
|
27
31
|
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 100
|
|
34
|
+
target-version = "py39"
|
|
35
|
+
src = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
# 默认集(F 全部 + E4/E7/E9);后续需要可扩展(I/UP/B 等)。
|
|
39
|
+
select = ["E4", "E7", "E9", "F"]
|
|
40
|
+
|
|
41
|
+
[tool.mypy]
|
|
42
|
+
python_version = "3.9"
|
|
43
|
+
check_untyped_defs = false
|
|
44
|
+
warn_redundant_casts = true
|
|
45
|
+
no_implicit_optional = true
|
|
46
|
+
ignore_missing_imports = true
|
|
47
|
+
|
|
28
48
|
[tool.setuptools.packages.find]
|
|
29
49
|
where = ["src"]
|
|
@@ -7,7 +7,8 @@ import urllib.error
|
|
|
7
7
|
import urllib.request
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from skillnav.
|
|
10
|
+
from skillnav.error_hints import enrich_api_error, network_unreachable
|
|
11
|
+
from skillnav.errors import AuthError, NetworkError, SkillnavError
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def request_bytes(
|
|
@@ -35,7 +36,9 @@ def request_bytes(
|
|
|
35
36
|
return exc.code, exc.read(), dict(exc.headers.items())
|
|
36
37
|
except urllib.error.URLError as exc:
|
|
37
38
|
reason = getattr(exc, "reason", exc)
|
|
38
|
-
raise NetworkError(str(reason)) from exc
|
|
39
|
+
raise NetworkError.from_hint(network_unreachable(str(reason), registry=url)) from exc
|
|
40
|
+
except OSError as exc:
|
|
41
|
+
raise NetworkError.from_hint(network_unreachable(str(exc), registry=url)) from exc
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
def request_json(
|
|
@@ -69,12 +72,10 @@ def api_error_message(body: Any) -> str:
|
|
|
69
72
|
def raise_for_api_status(status: int, body: Any) -> None:
|
|
70
73
|
if status < 400:
|
|
71
74
|
return
|
|
72
|
-
|
|
75
|
+
hint = enrich_api_error(api_error_message(body), status=status, body=body)
|
|
73
76
|
if status in (401, 403):
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
raise AuthError(message)
|
|
77
|
-
raise SkillnavError(message)
|
|
77
|
+
raise AuthError(hint.summary, hint=hint)
|
|
78
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
def parse_content_disposition_filename(header: str | None) -> str | None:
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import sys
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Annotated, Any, Optional
|
|
8
7
|
|
|
@@ -16,14 +15,22 @@ from skillnav.api import (
|
|
|
16
15
|
request_bytes,
|
|
17
16
|
request_json,
|
|
18
17
|
)
|
|
19
|
-
from skillnav.config import get_profile, load_config, save_config
|
|
18
|
+
from skillnav.config import get_profile, load_config, resolve_profile_api_key, save_config
|
|
20
19
|
from skillnav.contributors import resolve_contributor_id
|
|
21
20
|
from skillnav.context import CliContext
|
|
21
|
+
from skillnav.error_hints import (
|
|
22
|
+
enrich_api_error,
|
|
23
|
+
enrich_usage_error,
|
|
24
|
+
hint_from_message,
|
|
25
|
+
invalid_api_key,
|
|
26
|
+
not_logged_in,
|
|
27
|
+
)
|
|
22
28
|
from skillnav.errors import (
|
|
23
29
|
EXIT_AUTH,
|
|
24
30
|
EXIT_BUSINESS,
|
|
25
31
|
EXIT_NETWORK,
|
|
26
32
|
EXIT_OK,
|
|
33
|
+
EXIT_USAGE,
|
|
27
34
|
AuthError,
|
|
28
35
|
NetworkError,
|
|
29
36
|
SkillnavError,
|
|
@@ -32,17 +39,16 @@ from skillnav.errors import (
|
|
|
32
39
|
from skillnav.output import (
|
|
33
40
|
emit_error,
|
|
34
41
|
emit_json,
|
|
42
|
+
filter_skill_body_version,
|
|
35
43
|
print_leaderboard,
|
|
36
44
|
print_report_version,
|
|
37
|
-
|
|
38
|
-
print_review_result,
|
|
45
|
+
print_inspection_result,
|
|
39
46
|
print_search_results,
|
|
40
47
|
print_skill_info,
|
|
41
48
|
print_skill_status,
|
|
42
|
-
print_virustotal_summary,
|
|
43
49
|
unwrap_resource_id,
|
|
44
50
|
)
|
|
45
|
-
from skillnav.packages import extract_zip_to_directory, package_to_base64
|
|
51
|
+
from skillnav.packages import extract_zip_to_directory, package_to_base64
|
|
46
52
|
from skillnav.publish_metadata import build_publish_metadata, read_frontmatter_hints, resolve_package_path
|
|
47
53
|
from skillnav.urls import join_registry_url, slug_path
|
|
48
54
|
|
|
@@ -61,20 +67,46 @@ def _ctx() -> CliContext:
|
|
|
61
67
|
return _state["ctx"]
|
|
62
68
|
|
|
63
69
|
|
|
70
|
+
def _normalize_skillnav_error(exc: SkillnavError) -> SkillnavError:
|
|
71
|
+
if exc.detail or exc.next_steps:
|
|
72
|
+
return exc
|
|
73
|
+
if isinstance(exc, UsageError):
|
|
74
|
+
hint = enrich_usage_error(exc.message)
|
|
75
|
+
else:
|
|
76
|
+
hint = hint_from_message(exc.message)
|
|
77
|
+
return type(exc)(hint.summary, hint=hint)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _emit_skillnav_error(exc: SkillnavError, *, json_output: bool) -> None:
|
|
81
|
+
emit_error(
|
|
82
|
+
exc.message,
|
|
83
|
+
json_output=json_output,
|
|
84
|
+
detail=exc.detail,
|
|
85
|
+
next_steps=exc.next_steps,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
64
89
|
def _handle_error(exc: BaseException) -> None:
|
|
65
90
|
ctx = _state.get("ctx")
|
|
66
91
|
json_output = bool(ctx and ctx.json_output)
|
|
92
|
+
|
|
93
|
+
if isinstance(exc, ValueError):
|
|
94
|
+
hint = enrich_usage_error(str(exc))
|
|
95
|
+
wrapped = UsageError(hint.summary, hint=hint)
|
|
96
|
+
_emit_skillnav_error(wrapped, json_output=json_output)
|
|
97
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
98
|
+
|
|
67
99
|
if isinstance(exc, AuthError):
|
|
68
|
-
|
|
100
|
+
_emit_skillnav_error(_normalize_skillnav_error(exc), json_output=json_output)
|
|
69
101
|
raise typer.Exit(EXIT_AUTH) from exc
|
|
70
102
|
if isinstance(exc, UsageError):
|
|
71
|
-
|
|
72
|
-
raise typer.Exit(
|
|
103
|
+
_emit_skillnav_error(_normalize_skillnav_error(exc), json_output=json_output)
|
|
104
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
73
105
|
if isinstance(exc, NetworkError):
|
|
74
|
-
|
|
106
|
+
_emit_skillnav_error(exc, json_output=json_output)
|
|
75
107
|
raise typer.Exit(EXIT_NETWORK) from exc
|
|
76
108
|
if isinstance(exc, SkillnavError):
|
|
77
|
-
|
|
109
|
+
_emit_skillnav_error(_normalize_skillnav_error(exc), json_output=json_output)
|
|
78
110
|
raise typer.Exit(EXIT_BUSINESS) from exc
|
|
79
111
|
raise exc
|
|
80
112
|
|
|
@@ -92,16 +124,6 @@ def _api_json(method: str, path: str, *, body: dict | None = None, auth: bool =
|
|
|
92
124
|
return payload
|
|
93
125
|
|
|
94
126
|
|
|
95
|
-
def _friendly_publish_error(message: str) -> str:
|
|
96
|
-
if message == "skill_in_recycle_bin":
|
|
97
|
-
return "Skill is in the recycle bin — restore it on the web UI before publishing."
|
|
98
|
-
if message == "Only skill contributors can publish new versions":
|
|
99
|
-
return "Only skill contributors can publish new versions — ask the owner to add you."
|
|
100
|
-
if message == "review_pipeline_incomplete":
|
|
101
|
-
return "Review pipeline incomplete (503) — retry in a moment."
|
|
102
|
-
return message
|
|
103
|
-
|
|
104
|
-
|
|
105
127
|
def _version_callback(value: bool) -> None:
|
|
106
128
|
if value:
|
|
107
129
|
typer.echo(f"skillnav {__version__}")
|
|
@@ -109,7 +131,7 @@ def _version_callback(value: bool) -> None:
|
|
|
109
131
|
|
|
110
132
|
|
|
111
133
|
@app.callback()
|
|
112
|
-
def
|
|
134
|
+
def cli_root(
|
|
113
135
|
ctx: typer.Context,
|
|
114
136
|
registry: Annotated[
|
|
115
137
|
Optional[str], typer.Option("--registry", help="API base URL (overrides profile)")
|
|
@@ -132,7 +154,7 @@ def main(
|
|
|
132
154
|
),
|
|
133
155
|
] = None,
|
|
134
156
|
) -> None:
|
|
135
|
-
"""CLI client for the Skill management platform (
|
|
157
|
+
"""CLI client for the Skill management platform (SkillNavigator)."""
|
|
136
158
|
_state["ctx"] = CliContext.resolve(
|
|
137
159
|
registry_flag=registry,
|
|
138
160
|
profile_flag=profile,
|
|
@@ -172,7 +194,7 @@ def config_use(name: Annotated[str, typer.Argument(help="Profile name to activat
|
|
|
172
194
|
cli = _ctx()
|
|
173
195
|
config = load_config()
|
|
174
196
|
if name not in config.get("profiles", {}):
|
|
175
|
-
raise UsageError(f"Unknown profile: {name}")
|
|
197
|
+
raise UsageError.from_hint(enrich_usage_error(f"Unknown profile: {name}"))
|
|
176
198
|
config["defaultProfile"] = name
|
|
177
199
|
save_config(config)
|
|
178
200
|
if cli.json_output:
|
|
@@ -216,7 +238,8 @@ def config_test(
|
|
|
216
238
|
registry = profile.get("registry") or cli.registry
|
|
217
239
|
status, body = request_json("GET", join_registry_url(registry, "/health"))
|
|
218
240
|
if status >= 400:
|
|
219
|
-
|
|
241
|
+
hint = enrich_api_error(api_error_message(body), status=status, body=body)
|
|
242
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
220
243
|
if cli.json_output:
|
|
221
244
|
emit_json(body)
|
|
222
245
|
else:
|
|
@@ -249,17 +272,17 @@ def login_cmd(
|
|
|
249
272
|
credential = api_key
|
|
250
273
|
if not credential:
|
|
251
274
|
if cli.no_input:
|
|
252
|
-
raise AuthError(
|
|
275
|
+
raise AuthError.from_hint(not_logged_in(profile=cli.profile_name))
|
|
253
276
|
credential = typer.prompt("API key", hide_input=True).strip()
|
|
254
277
|
if not credential:
|
|
255
|
-
raise AuthError(
|
|
278
|
+
raise AuthError.from_hint(not_logged_in(profile=cli.profile_name))
|
|
256
279
|
status, me_body = request_json(
|
|
257
280
|
"GET",
|
|
258
281
|
join_registry_url(cli.registry, "/auth/me"),
|
|
259
282
|
token=credential,
|
|
260
283
|
)
|
|
261
284
|
if status >= 400:
|
|
262
|
-
raise AuthError(
|
|
285
|
+
raise AuthError.from_hint(invalid_api_key())
|
|
263
286
|
cli.persist_api_key(credential, me_body)
|
|
264
287
|
user = me_body.get("user") or {}
|
|
265
288
|
if cli.json_output:
|
|
@@ -375,8 +398,14 @@ def info_cmd(slug: Annotated[str, typer.Argument(help="Skill slug")]) -> None:
|
|
|
375
398
|
|
|
376
399
|
|
|
377
400
|
@app.command("status")
|
|
378
|
-
def status_cmd(
|
|
379
|
-
|
|
401
|
+
def status_cmd(
|
|
402
|
+
slug: Annotated[str, typer.Argument(help="Skill slug")],
|
|
403
|
+
version: Annotated[
|
|
404
|
+
Optional[str],
|
|
405
|
+
typer.Option("--version", help="Version to show (default: latest)"),
|
|
406
|
+
] = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Show publish and inspection status for one skill version."""
|
|
380
409
|
try:
|
|
381
410
|
cli = _ctx()
|
|
382
411
|
status, body = request_json(
|
|
@@ -385,10 +414,14 @@ def status_cmd(slug: Annotated[str, typer.Argument(help="Skill slug")]) -> None:
|
|
|
385
414
|
token=cli.token,
|
|
386
415
|
)
|
|
387
416
|
raise_for_api_status(status, body)
|
|
417
|
+
target_version = version or body.get("latestVersion")
|
|
418
|
+
if not target_version:
|
|
419
|
+
raise SkillnavError(f"No versions found for skill '{slug}'")
|
|
420
|
+
body = filter_skill_body_version(body, str(target_version))
|
|
388
421
|
if cli.json_output:
|
|
389
422
|
emit_json(body)
|
|
390
423
|
else:
|
|
391
|
-
print_skill_status(body)
|
|
424
|
+
print_skill_status(body, version=str(target_version))
|
|
392
425
|
except Exception as exc: # noqa: BLE001
|
|
393
426
|
_handle_error(exc)
|
|
394
427
|
|
|
@@ -418,7 +451,28 @@ def report_cmd(
|
|
|
418
451
|
_handle_error(exc)
|
|
419
452
|
|
|
420
453
|
|
|
421
|
-
# --- publish
|
|
454
|
+
# --- publish ---
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _print_publish_response(status: int, payload: dict[str, Any], *, waited: bool) -> None:
|
|
458
|
+
if status == 202 or payload.get("inspectionStatus") == "inspecting":
|
|
459
|
+
typer.echo(
|
|
460
|
+
f"Uploaded {payload.get('name')} ({payload.get('slug')})@{payload.get('version')}"
|
|
461
|
+
)
|
|
462
|
+
if waited:
|
|
463
|
+
typer.echo("Inspection started (inspectionStatus: inspecting).")
|
|
464
|
+
else:
|
|
465
|
+
typer.echo("Inspection started in the background (inspectionStatus: inspecting).")
|
|
466
|
+
typer.echo(f"Check progress: skillnav status {payload.get('slug')}")
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
typer.echo(
|
|
470
|
+
f"Published {payload.get('name')} ({payload.get('slug')})@{payload.get('version')}"
|
|
471
|
+
)
|
|
472
|
+
typer.echo(f"Status: {payload.get('status')}")
|
|
473
|
+
typer.echo(f"Hash: {payload.get('contentHash')}")
|
|
474
|
+
if payload.get("inspection") or payload.get("review") or payload.get("evaluation"):
|
|
475
|
+
print_inspection_result(payload)
|
|
422
476
|
|
|
423
477
|
|
|
424
478
|
@app.command("publish")
|
|
@@ -443,8 +497,15 @@ def publish_cmd(
|
|
|
443
497
|
] = None,
|
|
444
498
|
changelog: Annotated[Optional[str], typer.Option("--changelog", help="Changelog text")] = None,
|
|
445
499
|
dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview without publishing")] = False,
|
|
500
|
+
wait: Annotated[
|
|
501
|
+
bool,
|
|
502
|
+
typer.Option(
|
|
503
|
+
"--wait",
|
|
504
|
+
help="Wait for the full inspection pipeline to finish before returning (default: upload only, inspection in background)",
|
|
505
|
+
),
|
|
506
|
+
] = False,
|
|
446
507
|
) -> None:
|
|
447
|
-
"""
|
|
508
|
+
"""Upload a skill package to the registry; inspection runs in the background unless --wait."""
|
|
448
509
|
try:
|
|
449
510
|
cli = _ctx()
|
|
450
511
|
token = cli.require_token()
|
|
@@ -466,6 +527,8 @@ def publish_cmd(
|
|
|
466
527
|
body: dict[str, Any] = {"archiveBase64": archive_base64, "metadata": metadata}
|
|
467
528
|
if changelog:
|
|
468
529
|
body["changelog"] = changelog
|
|
530
|
+
if not dry_run:
|
|
531
|
+
body["async"] = not wait
|
|
469
532
|
path = "/skills/publish/preview" if dry_run else "/skills/publish"
|
|
470
533
|
status, payload = request_json(
|
|
471
534
|
method="POST",
|
|
@@ -474,8 +537,8 @@ def publish_cmd(
|
|
|
474
537
|
token=token,
|
|
475
538
|
)
|
|
476
539
|
if status >= 400:
|
|
477
|
-
|
|
478
|
-
raise SkillnavError(
|
|
540
|
+
hint = enrich_api_error(api_error_message(payload), status=status, body=payload)
|
|
541
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
479
542
|
if cli.json_output:
|
|
480
543
|
emit_json(payload if not dry_run else {"metadata": metadata, **payload})
|
|
481
544
|
return
|
|
@@ -490,41 +553,39 @@ def publish_cmd(
|
|
|
490
553
|
if payload.get("entryPath"):
|
|
491
554
|
typer.echo(f"Entry: {payload['entryPath']}")
|
|
492
555
|
return
|
|
493
|
-
|
|
494
|
-
f"Published {payload.get('name')} ({payload.get('slug')})@{payload.get('version')}"
|
|
495
|
-
)
|
|
496
|
-
typer.echo(f"Status: {payload.get('status')}")
|
|
497
|
-
typer.echo(f"Hash: {payload.get('contentHash')}")
|
|
498
|
-
if payload.get("review") or payload.get("evaluation"):
|
|
499
|
-
print_review_result(payload)
|
|
556
|
+
_print_publish_response(status, payload, waited=wait)
|
|
500
557
|
except Exception as exc: # noqa: BLE001
|
|
501
558
|
_handle_error(exc)
|
|
502
559
|
|
|
503
560
|
|
|
504
|
-
@app.command("
|
|
505
|
-
def
|
|
506
|
-
|
|
507
|
-
|
|
561
|
+
@app.command("retry-publish")
|
|
562
|
+
def retry_publish_cmd(
|
|
563
|
+
slug: Annotated[str, typer.Argument(help="Skill slug")],
|
|
564
|
+
wait: Annotated[
|
|
565
|
+
bool,
|
|
566
|
+
typer.Option(
|
|
567
|
+
"--wait",
|
|
568
|
+
help="Wait for the full inspection pipeline to finish before returning",
|
|
569
|
+
),
|
|
570
|
+
] = False,
|
|
508
571
|
) -> None:
|
|
509
|
-
"""
|
|
572
|
+
"""Re-run inspection on the stored package after a failed or incomplete inspection."""
|
|
510
573
|
try:
|
|
511
574
|
cli = _ctx()
|
|
512
|
-
|
|
513
|
-
body: dict[str, Any] = {"archiveBase64": archive_base64}
|
|
514
|
-
if version:
|
|
515
|
-
body["version"] = version
|
|
575
|
+
token = cli.require_token()
|
|
516
576
|
status, payload = request_json(
|
|
517
|
-
"POST",
|
|
518
|
-
join_registry_url(cli.registry, "/
|
|
519
|
-
body=
|
|
520
|
-
token=
|
|
577
|
+
method="POST",
|
|
578
|
+
url=join_registry_url(cli.registry, f"/skills/{slug_path(slug)}/retry-publish"),
|
|
579
|
+
body={"async": not wait},
|
|
580
|
+
token=token,
|
|
521
581
|
)
|
|
522
|
-
|
|
582
|
+
if status >= 400:
|
|
583
|
+
hint = enrich_api_error(api_error_message(payload), status=status, body=payload)
|
|
584
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
523
585
|
if cli.json_output:
|
|
524
586
|
emit_json(payload)
|
|
525
587
|
return
|
|
526
|
-
|
|
527
|
-
print_review_result(payload)
|
|
588
|
+
_print_publish_response(status, payload, waited=wait)
|
|
528
589
|
except Exception as exc: # noqa: BLE001
|
|
529
590
|
_handle_error(exc)
|
|
530
591
|
|
|
@@ -557,9 +618,11 @@ def download_cmd(
|
|
|
557
618
|
import json
|
|
558
619
|
|
|
559
620
|
payload = json.loads(data.decode("utf-8"))
|
|
560
|
-
|
|
621
|
+
hint = enrich_api_error(api_error_message(payload), status=status, body=payload)
|
|
622
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
561
623
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
562
|
-
|
|
624
|
+
hint = enrich_api_error(f"download failed ({status})", status=status)
|
|
625
|
+
raise SkillnavError(hint.summary, hint=hint) from None
|
|
563
626
|
filename = parse_content_disposition_filename(headers.get("Content-Disposition"))
|
|
564
627
|
out_path = output or Path(f"{slug}-{version}.zip")
|
|
565
628
|
out_path.write_bytes(data)
|
|
@@ -596,9 +659,11 @@ def install_cmd(
|
|
|
596
659
|
|
|
597
660
|
try:
|
|
598
661
|
payload = json.loads(data.decode("utf-8"))
|
|
599
|
-
|
|
662
|
+
hint = enrich_api_error(api_error_message(payload), status=status, body=payload)
|
|
663
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
600
664
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
601
|
-
|
|
665
|
+
hint = enrich_api_error(f"download failed ({status})", status=status)
|
|
666
|
+
raise SkillnavError(hint.summary, hint=hint) from None
|
|
602
667
|
target = dir or Path(slug)
|
|
603
668
|
extract_zip_to_directory(data, target)
|
|
604
669
|
if cli.json_output:
|
|
@@ -739,9 +804,11 @@ def remove_contributor_cmd(
|
|
|
739
804
|
"""Remove a contributor (owner only)."""
|
|
740
805
|
try:
|
|
741
806
|
if not id and not username:
|
|
742
|
-
raise UsageError("specify --id or --username")
|
|
807
|
+
raise UsageError.from_hint(enrich_usage_error("specify --id or --username"))
|
|
743
808
|
if id and username:
|
|
744
|
-
raise UsageError(
|
|
809
|
+
raise UsageError.from_hint(
|
|
810
|
+
enrich_usage_error("specify only one of --id or --username")
|
|
811
|
+
)
|
|
745
812
|
|
|
746
813
|
cli = _ctx()
|
|
747
814
|
token = cli.require_token()
|
|
@@ -755,6 +822,9 @@ def remove_contributor_cmd(
|
|
|
755
822
|
raise_for_api_status(status, skill_body)
|
|
756
823
|
contributor_id = resolve_contributor_id(skill_body, username)
|
|
757
824
|
|
|
825
|
+
if contributor_id is None:
|
|
826
|
+
raise UsageError.from_hint(enrich_usage_error("no contributor id available to remove"))
|
|
827
|
+
|
|
758
828
|
status, payload = request_json(
|
|
759
829
|
"DELETE",
|
|
760
830
|
join_registry_url(
|
|
@@ -808,7 +878,12 @@ def run(argv: list[str] | None = None) -> int:
|
|
|
808
878
|
except typer.Exit as exc:
|
|
809
879
|
return int(exc.exit_code)
|
|
810
880
|
except KeyboardInterrupt:
|
|
811
|
-
emit_error(
|
|
881
|
+
emit_error(
|
|
882
|
+
"interrupted",
|
|
883
|
+
json_output=bool(_state.get("ctx") and _state["ctx"].json_output),
|
|
884
|
+
detail="Command cancelled by user (Ctrl+C).",
|
|
885
|
+
next_steps=("Retry the command when ready.",),
|
|
886
|
+
)
|
|
812
887
|
return 130
|
|
813
888
|
|
|
814
889
|
|
|
@@ -14,6 +14,7 @@ from skillnav.config import (
|
|
|
14
14
|
save_config,
|
|
15
15
|
set_profile_identity,
|
|
16
16
|
)
|
|
17
|
+
from skillnav.error_hints import not_logged_in
|
|
17
18
|
from skillnav.errors import AuthError
|
|
18
19
|
|
|
19
20
|
|
|
@@ -68,7 +69,7 @@ class CliContext:
|
|
|
68
69
|
|
|
69
70
|
def require_token(self) -> str:
|
|
70
71
|
if not self.api_key:
|
|
71
|
-
raise AuthError(
|
|
72
|
+
raise AuthError.from_hint(not_logged_in(profile=self.profile_name))
|
|
72
73
|
return self.api_key
|
|
73
74
|
|
|
74
75
|
def persist_api_key(self, api_key: str, me_body: dict) -> None:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from skillnav.error_hints import enrich_usage_error
|
|
7
8
|
from skillnav.errors import SkillnavError
|
|
8
9
|
|
|
9
10
|
|
|
@@ -11,7 +12,9 @@ def resolve_contributor_id(skill_body: dict[str, Any], username: str) -> str:
|
|
|
11
12
|
"""Resolve a contributor id by username or display name (case-insensitive)."""
|
|
12
13
|
needle = username.strip().casefold()
|
|
13
14
|
if not needle:
|
|
14
|
-
raise SkillnavError(
|
|
15
|
+
raise SkillnavError.from_hint(
|
|
16
|
+
enrich_usage_error("username must not be empty")
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
matches: list[dict[str, Any]] = []
|
|
17
20
|
for contributor in skill_body.get("contributors") or []:
|
|
@@ -25,10 +28,16 @@ def resolve_contributor_id(skill_body: dict[str, Any], username: str) -> str:
|
|
|
25
28
|
matches.append(contributor)
|
|
26
29
|
|
|
27
30
|
if not matches:
|
|
28
|
-
raise SkillnavError(
|
|
31
|
+
raise SkillnavError.from_hint(
|
|
32
|
+
enrich_usage_error(f"contributor not found: {username}")
|
|
33
|
+
)
|
|
29
34
|
if len(matches) > 1:
|
|
30
|
-
raise SkillnavError(
|
|
35
|
+
raise SkillnavError.from_hint(
|
|
36
|
+
enrich_usage_error(f"ambiguous contributor: {username}")
|
|
37
|
+
)
|
|
31
38
|
contributor_id = matches[0].get("id")
|
|
32
39
|
if not contributor_id:
|
|
33
|
-
raise SkillnavError(
|
|
40
|
+
raise SkillnavError.from_hint(
|
|
41
|
+
enrich_usage_error(f"contributor not found: {username}")
|
|
42
|
+
)
|
|
34
43
|
return str(contributor_id)
|