chore: import upstream snapshot with attribution
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
"""
|
||||
CLI Skill Commands
|
||||
==================
|
||||
|
||||
Manage local skills and install packages from external hubs (EduHub, ClawHub, …).
|
||||
|
||||
Hub references use ``<hub>:<slug>[@version]``; the hub prefix defaults to
|
||||
``eduhub`` (DeepTutor's native open skill registry). Installs run the full
|
||||
import gate: hub security verdict →
|
||||
safe extraction → frontmatter adaptation (``always`` stripped, flat
|
||||
``bins``/``env`` folded into ``requires``) → provenance in ``.hub-lock.json``.
|
||||
|
||||
In a multi-user deployment this CLI operates the owner (admin) workspace, so
|
||||
an installed skill lands in the admin catalog and stays invisible to other
|
||||
users until a grant assigns it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.table import Table
|
||||
import typer
|
||||
|
||||
from .common import console
|
||||
|
||||
|
||||
def _resolve_token(explicit: str | None, hub: str) -> str | None:
|
||||
"""Publish token precedence: --token → env → ``skill login`` store."""
|
||||
import os
|
||||
|
||||
from deeptutor.services.skill import credentials
|
||||
|
||||
return (
|
||||
explicit
|
||||
or os.environ.get("DEEPTUTOR_HUB_TOKEN")
|
||||
or os.environ.get("EDUHUB_TOKEN")
|
||||
or credentials.get_stored_token(hub)
|
||||
)
|
||||
|
||||
|
||||
def _summary_table(
|
||||
title: str,
|
||||
*,
|
||||
hub: str,
|
||||
slug: str,
|
||||
version: str,
|
||||
overrides: dict[str, str],
|
||||
) -> Table:
|
||||
"""Confirmation table for the resolved classification (publish/update)."""
|
||||
from deeptutor.services.skill.taxonomy import domain_label, track_label
|
||||
|
||||
def shown(key: str, empty: str, *, as_domain: bool = False) -> str:
|
||||
vals = [v for v in overrides.get(key, "").split(",") if v]
|
||||
if not vals:
|
||||
return empty
|
||||
return "、".join(domain_label(v) if as_domain else v for v in vals)
|
||||
|
||||
table = Table(title=title, show_header=False, title_style="bold")
|
||||
table.add_column("字段", style="dim")
|
||||
table.add_column("值")
|
||||
table.add_row("hub", hub)
|
||||
table.add_row("slug", slug)
|
||||
table.add_row("version", version or "[red]缺失[/]")
|
||||
track = overrides.get("track", "")
|
||||
table.add_row("track", f"{track_label(track)} [dim]{track}[/]" if track else "[red]缺失[/]")
|
||||
table.add_row("language", overrides.get("language", ""))
|
||||
table.add_row("domains", shown("domains", "[dim]通用[/]", as_domain=True))
|
||||
table.add_row("stages", shown("stages", "[dim]全学段[/]"))
|
||||
table.add_row("forms", shown("forms", "[dim]—[/]"))
|
||||
table.add_row("audiences", shown("audiences", "[dim]学习者[/]"))
|
||||
table.add_row("tags", shown("tags", "[dim]—[/]"))
|
||||
return table
|
||||
|
||||
|
||||
def register(app: typer.Typer) -> None:
|
||||
@app.command("search")
|
||||
def skill_search(
|
||||
query: str = typer.Argument(..., help="Natural-language search query."),
|
||||
hub: str | None = typer.Option(
|
||||
None, "--hub", help="Hub to search (default from settings)."
|
||||
),
|
||||
limit: int = typer.Option(10, "--limit", min=1, max=50, help="Max results."),
|
||||
) -> None:
|
||||
"""Search a skill hub."""
|
||||
from deeptutor.services.skill.hub import HubError, default_hub, get_hub_provider
|
||||
|
||||
target = (hub or default_hub()).strip().lower()
|
||||
try:
|
||||
refs = get_hub_provider(target).search(query, limit=limit)
|
||||
except HubError as exc:
|
||||
console.print(f"[bold red]Search failed:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
if not refs:
|
||||
console.print("[dim]No skills matched.[/]")
|
||||
return
|
||||
table = Table(title=f"{target}: {query}")
|
||||
table.add_column("Ref", style="bold")
|
||||
table.add_column("Version")
|
||||
table.add_column("Summary")
|
||||
for ref in refs:
|
||||
table.add_row(
|
||||
f"{ref.hub}:{ref.slug}",
|
||||
ref.version or "-",
|
||||
ref.summary[:100] or ref.display_name,
|
||||
)
|
||||
console.print(table)
|
||||
console.print("[dim]Install with: deeptutor skill install <ref>[/]")
|
||||
|
||||
@app.command("install")
|
||||
def skill_install(
|
||||
ref: str = typer.Argument(..., help="Skill ref: <hub>:<slug>[@version]."),
|
||||
name: str | None = typer.Option(
|
||||
None, "--name", help="Install under a different local skill name."
|
||||
),
|
||||
force: bool = typer.Option(
|
||||
False, "--force", help="Overwrite an existing skill with the same name."
|
||||
),
|
||||
allow_unverified: bool = typer.Option(
|
||||
False,
|
||||
"--allow-unverified",
|
||||
help="Install even when the hub flags the package as suspicious.",
|
||||
),
|
||||
) -> None:
|
||||
"""Install a skill from a hub into the local skill library."""
|
||||
from deeptutor.services.skill.hub import HubError, install_from_hub
|
||||
from deeptutor.services.skill.service import (
|
||||
InvalidSkillNameError,
|
||||
SkillExistsError,
|
||||
SkillImportError,
|
||||
get_skill_service,
|
||||
)
|
||||
|
||||
service = get_skill_service()
|
||||
try:
|
||||
outcome = install_from_hub(
|
||||
ref,
|
||||
service=service,
|
||||
rename_to=name,
|
||||
force=force,
|
||||
allow_unverified=allow_unverified,
|
||||
)
|
||||
except SkillExistsError as exc:
|
||||
console.print(
|
||||
f"[bold red]Skill `{exc}` already exists.[/] Re-run with --force to replace it."
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
except (HubError, SkillImportError, InvalidSkillNameError) as exc:
|
||||
console.print(f"[bold red]Install failed:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
info = outcome.result.info
|
||||
verdict = outcome.verdict
|
||||
verdict_style = {"ok": "green", "suspicious": "red"}.get(verdict.status, "yellow")
|
||||
console.print(
|
||||
f"[bold green]Installed[/] [bold]{info.name}[/]"
|
||||
+ (f" [dim]({outcome.ref.hub}@{outcome.ref.version})[/]" if outcome.ref.version else "")
|
||||
)
|
||||
console.print(
|
||||
f" verdict: [{verdict_style}]{verdict.status}[/]"
|
||||
+ (f" [dim]{verdict.detail}[/]" if verdict.detail else "")
|
||||
)
|
||||
if verdict.status != "ok":
|
||||
console.print(
|
||||
f" [yellow]Review before use:[/] [dim]{service.root / info.name / 'SKILL.md'}[/]"
|
||||
)
|
||||
for entry in service.summary_entries():
|
||||
if entry.name == info.name and not entry.available:
|
||||
console.print(f" [yellow]unavailable until:[/] {', '.join(entry.missing)}")
|
||||
for rel, reason in outcome.result.skipped:
|
||||
console.print(f" [dim]skipped {rel} — {reason}[/]")
|
||||
|
||||
@app.command("login")
|
||||
def skill_login(
|
||||
provider: str | None = typer.Argument(
|
||||
None, help="登录方式:github | google(不填则在终端询问)。"
|
||||
),
|
||||
hub: str | None = typer.Option(None, "--hub", help="Target hub (default from settings)."),
|
||||
no_browser: bool = typer.Option(
|
||||
False, "--no-browser", help="不自动打开浏览器,只打印授权链接。"
|
||||
),
|
||||
) -> None:
|
||||
"""浏览器授权登录到 skill hub,令牌保存在本地供 publish / update 使用。"""
|
||||
from deeptutor.services.skill import credentials
|
||||
from deeptutor.services.skill.hub import HubError, default_hub, get_hub_provider
|
||||
from deeptutor.services.skill.taxonomy import Option
|
||||
|
||||
from .skill_login import hub_origin_from_base, run_login
|
||||
from .skill_prompts import select_one
|
||||
|
||||
target_hub = (hub or default_hub()).strip().lower()
|
||||
try:
|
||||
provider_obj = get_hub_provider(target_hub)
|
||||
except HubError as exc:
|
||||
console.print(f"[bold red]{exc}[/]")
|
||||
raise typer.Exit(code=1)
|
||||
base_url = getattr(provider_obj, "base_url", None)
|
||||
if not base_url:
|
||||
console.print(f"[bold red]Hub `{target_hub}` 不支持网页登录(非 clawhub 型)。[/]")
|
||||
raise typer.Exit(code=1)
|
||||
origin = hub_origin_from_base(str(base_url))
|
||||
|
||||
prov = (provider or "").strip().lower()
|
||||
if prov not in ("github", "google"):
|
||||
prov = select_one(
|
||||
[Option("github", "GitHub", "GitHub"), Option("google", "Google", "Google")],
|
||||
"选择登录方式",
|
||||
)
|
||||
|
||||
console.print(f"[dim]在浏览器完成 {prov} 授权(hub: {target_hub})…[/]")
|
||||
result = run_login(
|
||||
origin,
|
||||
prov,
|
||||
on_url=lambda u: console.print(
|
||||
f" 若浏览器未自动打开,请手动访问:\n [underline]{u}[/]"
|
||||
),
|
||||
open_browser=not no_browser,
|
||||
)
|
||||
if result.error or not result.token:
|
||||
console.print(f"[bold red]登录失败:[/] {result.error or '未收到令牌'}")
|
||||
raise typer.Exit(code=1)
|
||||
try:
|
||||
credentials.store_token(target_hub, result.token, login=result.login)
|
||||
except RuntimeError as exc:
|
||||
console.print(f"[bold red]令牌保存失败:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
who = f" as @{result.login}" if result.login else ""
|
||||
console.print(f"[bold green]已登录[/] {target_hub}{who},令牌已保存到本地。")
|
||||
|
||||
@app.command("logout")
|
||||
def skill_logout(
|
||||
hub: str | None = typer.Option(None, "--hub", help="Target hub (default from settings)."),
|
||||
) -> None:
|
||||
"""清除本地保存的某个 hub 登录令牌。"""
|
||||
from deeptutor.services.skill import credentials
|
||||
from deeptutor.services.skill.hub import default_hub
|
||||
|
||||
target_hub = (hub or default_hub()).strip().lower()
|
||||
if credentials.clear_token(target_hub):
|
||||
console.print(f"[green]已登出[/] {target_hub}。")
|
||||
else:
|
||||
console.print(f"[dim]{target_hub} 本来就没有保存的令牌。[/]")
|
||||
|
||||
@app.command("publish")
|
||||
def skill_publish(
|
||||
directory: str = typer.Argument(..., help="Skill directory containing SKILL.md."),
|
||||
version: str | None = typer.Option(
|
||||
None, "--version", help="semver to publish; overrides SKILL.md `version:`."
|
||||
),
|
||||
slug: str | None = typer.Option(
|
||||
None, "--slug", help="Override slug (default: SKILL.md slug/name)."
|
||||
),
|
||||
track: str | None = typer.Option(
|
||||
None, "--track", help="Track (skips the track prompt when set)."
|
||||
),
|
||||
hub: str | None = typer.Option(None, "--hub", help="Target hub (default from settings)."),
|
||||
token: str | None = typer.Option(
|
||||
None, "--token", help="Publish token; else env / `deeptutor skills login`."
|
||||
),
|
||||
yes: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Non-interactive: take classification from SKILL.md/flags as-is (CI).",
|
||||
),
|
||||
) -> None:
|
||||
"""Publish a skill: interactively tag it (track + facets), then upload.
|
||||
|
||||
Pre-flights the package format, walks the required track (one-of) and
|
||||
optional facets — pre-filled from SKILL.md frontmatter — shows a summary
|
||||
for confirmation, then publishes. ``--yes`` skips the prompts for CI.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from deeptutor.services.skill.hub import (
|
||||
HubError,
|
||||
default_hub,
|
||||
preflight_skill_dir,
|
||||
publish_to_hub,
|
||||
read_skill_metadata,
|
||||
resolve_publish_identity,
|
||||
)
|
||||
from deeptutor.services.skill.service import SkillImportError
|
||||
|
||||
from .skill_prompts import collect_classification
|
||||
|
||||
target_hub = (hub or default_hub()).strip().lower()
|
||||
|
||||
# ── step 3: local format pre-flight ────────────────────────────────
|
||||
pre = preflight_skill_dir(directory)
|
||||
for warning in pre.warnings:
|
||||
console.print(f"[yellow]⚠[/] {warning}")
|
||||
if not pre.ok:
|
||||
console.print("[bold red]格式预检未通过,请先修复:[/]")
|
||||
for err in pre.errors:
|
||||
console.print(f" [red]✗[/] {err}")
|
||||
console.print(" [dim]格式规范见 https://eduhub.deeptutor.info/skill-format.md[/]")
|
||||
raise typer.Exit(code=1)
|
||||
console.print(
|
||||
f"[green]✓[/] 格式预检通过 "
|
||||
f"[dim]({pre.file_count} 个文件, {pre.total_bytes // 1024} KB)[/]"
|
||||
)
|
||||
|
||||
fm = read_skill_metadata(directory)
|
||||
interactive = (not yes) and sys.stdin.isatty()
|
||||
overrides: dict[str, str] = {}
|
||||
eff_version = (version or str(fm.get("version") or "")).strip()
|
||||
|
||||
if interactive:
|
||||
# step 4: required track + optional facets, pre-filled from frontmatter
|
||||
overrides = collect_classification({**fm, "track": track or fm.get("track")})
|
||||
if not eff_version:
|
||||
eff_version = typer.prompt("\n版本号 version (semver)", default="1.0.0").strip()
|
||||
# step 5: confirmation summary
|
||||
preview_slug, _ = resolve_publish_identity(directory, slug=slug, version=eff_version)
|
||||
console.print(
|
||||
_summary_table(
|
||||
"确认发布信息",
|
||||
hub=target_hub,
|
||||
slug=preview_slug,
|
||||
version=eff_version,
|
||||
overrides=overrides,
|
||||
)
|
||||
)
|
||||
if not typer.confirm("确认提交?", default=True):
|
||||
console.print("[dim]已取消。[/]")
|
||||
raise typer.Exit(code=0)
|
||||
else:
|
||||
# Non-interactive (CI): track must come from --track or frontmatter.
|
||||
eff_track = track or str(fm.get("track") or "")
|
||||
if not eff_track:
|
||||
console.print(
|
||||
"[bold red]缺少 track。[/] 加 --track,或在 SKILL.md 写 track:,"
|
||||
"或去掉 --yes 走交互式打标。"
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
tok = _resolve_token(token, target_hub)
|
||||
if not tok:
|
||||
console.print(
|
||||
"[bold red]未登录。[/] 运行 [bold]deeptutor skills login[/] 完成浏览器授权,"
|
||||
"或用 --token / $DEEPTUTOR_HUB_TOKEN 传入令牌。"
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
try:
|
||||
outcome = publish_to_hub(
|
||||
directory,
|
||||
token=tok,
|
||||
version=eff_version or None,
|
||||
slug=slug,
|
||||
track=track,
|
||||
hub=target_hub,
|
||||
overrides=overrides or None,
|
||||
)
|
||||
except (HubError, SkillImportError) as exc:
|
||||
console.print(f"[bold red]Publish failed:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
console.print(
|
||||
f"[bold green]Published[/] [bold]{outcome.slug}@{outcome.version}[/] → {outcome.hub}"
|
||||
)
|
||||
console.print(
|
||||
f" [dim]install with:[/] deeptutor skill install {outcome.hub}:{outcome.slug}"
|
||||
)
|
||||
|
||||
@app.command("update")
|
||||
def skill_update(
|
||||
directory: str | None = typer.Argument(
|
||||
None, help="新版本的 skill 目录(升级新版本时用;回退不需要)。"
|
||||
),
|
||||
hub: str | None = typer.Option(None, "--hub", help="Target hub (default from settings)."),
|
||||
token: str | None = typer.Option(
|
||||
None, "--token", help="Publish token; else env / `deeptutor skills login`."
|
||||
),
|
||||
) -> None:
|
||||
"""维护已发布技能:列出我的技能 → 选一个 → 回退旧版本,或发布新版本。
|
||||
|
||||
升级新版本时,打标环节默认沿用该技能当前的 track 与各维度标签;回退则
|
||||
把 ``latest`` 指针指回某个更旧的已发布版本,不新建版本。
|
||||
"""
|
||||
import sys
|
||||
|
||||
from deeptutor.services.skill.hub import (
|
||||
HubError,
|
||||
default_hub,
|
||||
get_hub_provider,
|
||||
preflight_skill_dir,
|
||||
publish_to_hub,
|
||||
)
|
||||
from deeptutor.services.skill.service import SkillImportError
|
||||
from deeptutor.services.skill.taxonomy import Option
|
||||
|
||||
from .skill_prompts import collect_classification, select_one
|
||||
|
||||
target_hub = (hub or default_hub()).strip().lower()
|
||||
if not sys.stdin.isatty():
|
||||
console.print("[bold red]update 是交互式命令,请在终端中运行。[/]")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
tok = _resolve_token(token, target_hub)
|
||||
if not tok:
|
||||
console.print(
|
||||
"[bold red]未登录。[/] 运行 [bold]deeptutor skills login[/] 完成浏览器授权,"
|
||||
"或用 --token 传入令牌。"
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
provider = get_hub_provider(target_hub)
|
||||
lister = getattr(provider, "list_my_skills", None)
|
||||
if not callable(lister):
|
||||
console.print(f"[bold red]Hub `{target_hub}` 不支持列出已发布技能。[/]")
|
||||
raise typer.Exit(code=1)
|
||||
try:
|
||||
mine = [s for s in lister(tok) if s.get("slug")]
|
||||
except HubError as exc:
|
||||
console.print(f"[bold red]获取失败:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
if not mine:
|
||||
console.print(f"[dim]你还没有在 {target_hub} 发布过技能。[/]")
|
||||
raise typer.Exit(code=0)
|
||||
|
||||
# ── step 3: pick a skill, then pick the action ────────────────────
|
||||
skill_opts = [
|
||||
Option(
|
||||
str(s["slug"]),
|
||||
f"{s.get('displayName') or s['slug']} v{s.get('version') or '-'}",
|
||||
str(s["slug"]),
|
||||
)
|
||||
for s in mine
|
||||
]
|
||||
chosen_slug = select_one(skill_opts, "选择要更新的技能")
|
||||
chosen = next(s for s in mine if s.get("slug") == chosen_slug)
|
||||
versions = [str(v) for v in (chosen.get("versions") or [])]
|
||||
current = str(chosen.get("version") or "")
|
||||
|
||||
action = select_one(
|
||||
[
|
||||
Option("rollback", "回退版本(把 latest 指回旧版本)", "Roll back"),
|
||||
Option("upgrade", "发布新版本", "Publish a new version"),
|
||||
],
|
||||
f"对 {chosen_slug} 做什么?",
|
||||
)
|
||||
|
||||
# ── rollback: move latest to an older version ─────────────────────
|
||||
if action == "rollback":
|
||||
if len([v for v in versions if v != current]) == 0:
|
||||
console.print(f"[dim]{chosen_slug} 只有一个版本({current}),无法回退。[/]")
|
||||
raise typer.Exit(code=0)
|
||||
version_opts = [Option(v, "← 当前 latest" if v == current else "", v) for v in versions]
|
||||
target_version = select_one(version_opts, "回退 latest 到哪个版本?")
|
||||
if target_version == current:
|
||||
console.print("[dim]已是当前 latest,无需回退。[/]")
|
||||
raise typer.Exit(code=0)
|
||||
if not typer.confirm(
|
||||
f"把 {chosen_slug} 的 latest 从 {current or '-'} 回退到 {target_version}?",
|
||||
default=True,
|
||||
):
|
||||
console.print("[dim]已取消。[/]")
|
||||
raise typer.Exit(code=0)
|
||||
tag_setter = getattr(provider, "set_dist_tag", None)
|
||||
if not callable(tag_setter):
|
||||
console.print(f"[bold red]Hub `{target_hub}` 不支持回退 latest。[/]")
|
||||
raise typer.Exit(code=1)
|
||||
try:
|
||||
tag_setter(chosen_slug, version=target_version, token=tok)
|
||||
except HubError as exc:
|
||||
console.print(f"[bold red]回退失败:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
console.print(
|
||||
f"[bold green]已回退[/] {chosen_slug} 的 latest → [bold]{target_version}[/]"
|
||||
)
|
||||
return
|
||||
|
||||
# ── upgrade: publish a new version (tags default to current) ──────
|
||||
if not directory:
|
||||
directory = typer.prompt("新版本的 skill 目录路径").strip()
|
||||
pre = preflight_skill_dir(directory)
|
||||
for warning in pre.warnings:
|
||||
console.print(f"[yellow]⚠[/] {warning}")
|
||||
if not pre.ok:
|
||||
console.print("[bold red]格式预检未通过,请先修复:[/]")
|
||||
for err in pre.errors:
|
||||
console.print(f" [red]✗[/] {err}")
|
||||
raise typer.Exit(code=1)
|
||||
console.print(
|
||||
f"[green]✓[/] 格式预检通过 "
|
||||
f"[dim]({pre.file_count} 个文件, {pre.total_bytes // 1024} KB)[/]"
|
||||
)
|
||||
|
||||
overrides = collect_classification(chosen)
|
||||
new_version = typer.prompt(
|
||||
f"\n新版本号 version (semver,当前 latest = {current or '-'})"
|
||||
).strip()
|
||||
|
||||
console.print(
|
||||
_summary_table(
|
||||
"确认升级信息",
|
||||
hub=target_hub,
|
||||
slug=chosen_slug,
|
||||
version=new_version,
|
||||
overrides=overrides,
|
||||
)
|
||||
)
|
||||
if not typer.confirm("确认发布新版本?", default=True):
|
||||
console.print("[dim]已取消。[/]")
|
||||
raise typer.Exit(code=0)
|
||||
|
||||
try:
|
||||
outcome = publish_to_hub(
|
||||
directory,
|
||||
token=tok,
|
||||
version=new_version or None,
|
||||
slug=chosen_slug,
|
||||
hub=target_hub,
|
||||
overrides=overrides,
|
||||
)
|
||||
except (HubError, SkillImportError) as exc:
|
||||
console.print(f"[bold red]Update failed:[/] {exc}")
|
||||
raise typer.Exit(code=1)
|
||||
console.print(
|
||||
f"[bold green]Updated[/] [bold]{outcome.slug}@{outcome.version}[/] → {outcome.hub}"
|
||||
)
|
||||
|
||||
@app.command("list")
|
||||
def skill_list() -> None:
|
||||
"""List local skills, including hub provenance."""
|
||||
from deeptutor.services.skill.service import get_skill_service
|
||||
|
||||
service = get_skill_service()
|
||||
table = Table(title="Skills")
|
||||
table.add_column("Name", style="bold")
|
||||
table.add_column("Source")
|
||||
table.add_column("Origin")
|
||||
table.add_column("Description")
|
||||
for info in service.list_skills():
|
||||
origin = service.hub_origin(info.name)
|
||||
origin_label = "-"
|
||||
if origin:
|
||||
version = str(origin.get("version") or "").strip()
|
||||
origin_label = str(origin.get("hub") or "hub") + (f"@{version}" if version else "")
|
||||
table.add_row(info.name, info.source, origin_label, info.description[:80])
|
||||
console.print(table)
|
||||
|
||||
@app.command("remove")
|
||||
def skill_remove(
|
||||
name: str = typer.Argument(..., help="Skill name to remove."),
|
||||
) -> None:
|
||||
"""Remove a user-layer skill (builtin skills are read-only)."""
|
||||
from deeptutor.services.skill.service import (
|
||||
InvalidSkillNameError,
|
||||
SkillNotFoundError,
|
||||
SkillReadOnlyError,
|
||||
get_skill_service,
|
||||
)
|
||||
|
||||
try:
|
||||
get_skill_service().delete(name)
|
||||
except (SkillNotFoundError, InvalidSkillNameError):
|
||||
console.print(f"[bold red]Skill not found:[/] {name}")
|
||||
raise typer.Exit(code=1)
|
||||
except SkillReadOnlyError as exc:
|
||||
console.print(f"[bold red]{exc}[/]")
|
||||
raise typer.Exit(code=1)
|
||||
console.print(f"[green]Removed[/] {name}")
|
||||
Reference in New Issue
Block a user