skill推拉重构:输入成对引号剥离+输出固定+探查式覆盖+删除闭环,覆盖删除统一进.trash备份
This commit is contained in:
@@ -60,9 +60,12 @@ SKILL_ALIASES = {
|
||||
"1": "list", "1.": "list", "list": "list", "列表": "list", "查看": "list",
|
||||
"技能列表": "list", "看列表": "list",
|
||||
"2": "push", "2.": "push", "push": "push", "推送": "push", "上传": "push", "推": "push",
|
||||
"覆盖更新": "push", "更新": "push", "覆盖": "push",
|
||||
"3": "pull", "3.": "pull", "pull": "pull", "拉取": "pull", "下载": "pull", "拉": "pull",
|
||||
"4": "dirs", "4.": "dirs", "dirs": "dirs", "目录": "dirs", "状态": "dirs",
|
||||
"技能目录": "dirs",
|
||||
"5": "delete", "5.": "delete", "delete": "delete", "del": "delete",
|
||||
"删除": "delete", "删": "delete", "移除": "delete", "删掉": "delete",
|
||||
}
|
||||
|
||||
MCP_ALIASES = {
|
||||
@@ -151,19 +154,109 @@ def run_safe(fn, argv: list[str]) -> int:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 子菜单:技能管理
|
||||
def _yes(prompt: str) -> bool:
|
||||
"""统一的 y/N 判定(默认 N,回车即取消)。"""
|
||||
return match(ask(prompt), {"y": "y", "yes": "y", "是": "y"}, None) == "y"
|
||||
|
||||
def _flow_push() -> None:
|
||||
"""推送 / 覆盖更新:输入 -> 校验规范 -> 探查同名 -> 确有同名才问一次覆盖 -> 写入。
|
||||
|
||||
输出固定为 WSL OC 的 skill 目录,不由用户决定。
|
||||
目标没有同名文件夹时直接写(没有目录和内容,覆盖无从谈起,问了也是白问)。
|
||||
"""
|
||||
src = ocskill.clean_input(ask(" 技能输入(路径 / zip / SKILL.md,可带引号,回车取消): "))
|
||||
if not src:
|
||||
return
|
||||
# 1) 先过一遍规范校验(只读探查,不写盘)
|
||||
probe = ocskill.probe_push(src)
|
||||
if not probe.get("ok"):
|
||||
print(" 校验未通过:%s" % probe.get("err"))
|
||||
return
|
||||
name = probe["name"]
|
||||
desc = (probe.get("description") or "").replace("\n", " ")
|
||||
print(" 校验通过:「%s」" % name)
|
||||
if desc:
|
||||
print(" 描述:%s" % (desc[:90] + ("..." if len(desc) > 90 else "")))
|
||||
print(" 输出:%s(固定,不需选择)" % probe["dest"])
|
||||
# 2) 探查同名:不存在 -> 直接放,不问覆盖
|
||||
if not probe.get("exists"):
|
||||
print(" 目标无同名目录,直接推送。")
|
||||
run_safe(ocskill.main, ["push", src])
|
||||
return
|
||||
# 3) 确有同名文件夹 -> 这才有资格问一次覆盖
|
||||
if _yes(" 已存在同名 skill「%s」,覆盖?(y/N): " % name):
|
||||
print(" 覆盖更新(旧版本先挑进 .trash/ 备份)。")
|
||||
run_safe(ocskill.main, ["push", src, "--force"])
|
||||
else:
|
||||
print(" 已跳过覆盖,未改动任何文件。")
|
||||
|
||||
def _flow_pull() -> None:
|
||||
"""拉取:输入技能名 -> 探查 WSL 端存在 -> 探查本机同名 -> 确有同名才问一次覆盖 -> 写入。
|
||||
|
||||
输出固定为本机 OC 的 skill 目录,不由用户决定。
|
||||
"""
|
||||
name = ocskill.clean_input(ask(" 要拉取的技能名(可带引号,回车取消): "))
|
||||
if not name:
|
||||
return
|
||||
probe = ocskill.probe_pull(name)
|
||||
if not probe.get("ok"):
|
||||
print(" %s" % probe.get("err"))
|
||||
return
|
||||
print(" 输出:%s(固定,不需选择)" % probe["dest"])
|
||||
if not probe.get("exists"):
|
||||
print(" 本机无同名目录,直接拉取。")
|
||||
run_safe(ocskill.main, ["pull", name])
|
||||
return
|
||||
if _yes(" 本机已存在同名 skill「%s」,覆盖?(y/N): " % name):
|
||||
print(" 覆盖更新(旧版本先挑进 .trash/ 备份)。")
|
||||
run_safe(ocskill.main, ["pull", name, "--force"])
|
||||
else:
|
||||
print(" 已跳过覆盖,未改动任何文件。")
|
||||
|
||||
def _flow_delete() -> None:
|
||||
"""删除:输入技能名 -> 选端 -> 展示将删路径 -> 确认 -> 删除(先挑 .trash/ 可恢复)。"""
|
||||
name = ocskill.clean_input(ask(" 要删除的技能名(可带引号,回车取消): "))
|
||||
if not name:
|
||||
return
|
||||
side = match(
|
||||
ask(" 删哪端?(1=Wsl / 2=本机,回车取消): "),
|
||||
{"1": "wsl", "1.": "wsl", "wsl": "wsl", "ws": "wsl", "远程": "wsl", "wsl端": "wsl",
|
||||
"2": "local", "2.": "local", "local": "local", "本机": "local", "本地": "local", "本机端": "local"},
|
||||
None,
|
||||
)
|
||||
if side is None:
|
||||
print(" 已取消。")
|
||||
return
|
||||
local = side == "local"
|
||||
probe = ocskill.probe_delete(name, ocskill.DEFAULT_DISTRO, local)
|
||||
if not probe.get("ok"):
|
||||
print(" %s" % probe.get("err"))
|
||||
return
|
||||
print(" 将删除%s:%s" % (probe["side"], probe["dest"]))
|
||||
print(" (会先挑进 .trash/ 备份,删错了可移回)")
|
||||
if not _yes(" 确认删除?(y/N): "):
|
||||
print(" 已取消删除。")
|
||||
return
|
||||
argv = ["delete", name, "--yes"]
|
||||
if local:
|
||||
argv.append("--local")
|
||||
run_safe(ocskill.main, argv)
|
||||
|
||||
def menu_skill(shared: dict) -> None:
|
||||
while True:
|
||||
banner("技能管理(skill)")
|
||||
print(" 0. 返回上层")
|
||||
print(" 1. 查看技能列表")
|
||||
print(" 2. 推送技能到 OC(zip / 目录 / SKILL.md / base64)")
|
||||
print(" 3. 拉取技能到本机")
|
||||
print(" 2. 推送 / 覆盖更新技能到 WSL OC(输入即校验,输出固定为 WSL skill 目录)")
|
||||
print(" 3. 拉取技能到本机 OC(输出固定为本机 skill 目录)")
|
||||
print(" 4. 查看技能目录与连接状态")
|
||||
print(" 5. 删除技能(先挑 .trash/ 备份,可恢复)")
|
||||
print(" 提示:任意位置输入 /home 或「返回首页」直接回顶层菜单")
|
||||
print(" 覆盖只在确有同名文件夹时才会问你一次")
|
||||
raw = ask("请选择(编号或中文,/home 回首页): ")
|
||||
act = match(raw, SKILL_ALIASES)
|
||||
if act is None:
|
||||
print(" 没看懂「%s」,输入 0-4 或中文,如:列表 / 推送 / 拉取" % raw.strip())
|
||||
print(" 没看懂「%s」,输入 0-5 或中文,如:列表 / 推送 / 拉取 / 删除" % raw.strip())
|
||||
continue
|
||||
if act == "back":
|
||||
return
|
||||
@@ -173,26 +266,11 @@ def menu_skill(shared: dict) -> None:
|
||||
elif act == "dirs":
|
||||
run_safe(ocskill.main, ["dirs"])
|
||||
elif act == "push":
|
||||
src = ask(" 技能路径(文件 / 目录 / zip,回车取消): ").strip()
|
||||
if not src:
|
||||
continue
|
||||
argv = ["push", src]
|
||||
if match(ask(" 同名覆盖?(y/N): "), {"y": "y", "yes": "y", "是": "y"}, None) == "y":
|
||||
argv.append("--force")
|
||||
if match(ask(" 只校验不写入?(y/N): "), {"y": "y", "yes": "y", "是": "y"}, None) == "y":
|
||||
argv.append("--dry-run")
|
||||
run_safe(ocskill.main, argv)
|
||||
_flow_push()
|
||||
elif act == "pull":
|
||||
name = ask(" 要拉取的技能名(回车取消): ").strip()
|
||||
if not name:
|
||||
continue
|
||||
argv = ["pull", name]
|
||||
out = ask(" 输出目录(回车=当前目录): ").strip()
|
||||
if out:
|
||||
argv += ["--out", out]
|
||||
if match(ask(" 覆盖本地同名目录?(y/N): "), {"y": "y", "yes": "y", "是": "y"}, None) == "y":
|
||||
argv.append("--force")
|
||||
run_safe(ocskill.main, argv)
|
||||
_flow_pull()
|
||||
elif act == "delete":
|
||||
_flow_delete()
|
||||
pause()
|
||||
|
||||
|
||||
@@ -274,7 +352,7 @@ def menu_top() -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 直传模式
|
||||
SKILL_CMDS = {"list", "push", "pull", "dirs"}
|
||||
SKILL_CMDS = {"list", "push", "pull", "dirs", "delete"}
|
||||
MCP_CMDS = {"status", "on", "off", "mode", "backup"}
|
||||
|
||||
|
||||
|
||||
+186
-15
@@ -6,11 +6,18 @@ ocskill —— 打通本机与 WSL OpenCode 的 skill 互通
|
||||
做什么:
|
||||
list 运行时连接 WSL 里的 OpenCode,返回它的 skill 列表
|
||||
push 把一个新的 skill 文件(压缩包 / 目录 / 单个 SKILL.md / base64)
|
||||
先解码并校验是不是标准 skill 格式,然后写进 OC 的 skill 目录
|
||||
先解码并校验是不是标准 skill 格式,然后写进 WSL OC 的 skill 目录
|
||||
(压缩包会额外做一次解压;OC 自带目录 watch,写完即热加载)
|
||||
pull 反向操作:把 WSL 里的某个 skill 拷回本机
|
||||
pull 反向操作:把 WSL 里的某个 skill 拷回本机 OC 的 skill 目录
|
||||
delete 删除某个 skill(先挪进 .trash/ 备份,可恢复)
|
||||
dirs 显示两端 skill 目录位置
|
||||
|
||||
输入/输出约定(输出不由用户决定):
|
||||
输入 路径/技能名都过一遍 clean_input,粘贴带引号的完整路径也认
|
||||
输出 push -> WSL OC skill 目录;pull -> 本机 OC skill 目录
|
||||
覆盖 探查式:先看同名文件夹存不存在 —— 不存在就直接写(不问);
|
||||
确有同名才需要 --force,覆盖前统一挪进 .trash/<时间戳>/ 备份
|
||||
|
||||
零依赖:只用 Python 标准库(urllib / zipfile / shutil),无需 pip install。
|
||||
"""
|
||||
|
||||
@@ -417,6 +424,94 @@ def _frontmatter(text: str) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 输入净化与探查
|
||||
_QUOTE_PAIRS = (('"', '"'), ("'", "'"), ("\u201c", "\u201d"), ("\u300c", "\u300d"))
|
||||
|
||||
def clean_input(raw: str) -> str:
|
||||
"""剥掉用户粘贴时带上的引号并去首尾空白。
|
||||
|
||||
从终端/对话里复制完整 Windows 路径时,两端常带着引号
|
||||
(如 "C:\\Users\\...\\skills\\opencode-plugin-dev")。
|
||||
不剥掉就会把引号当路径的一部分,直接报「路径不存在」/
|
||||
「WSL 端不存在该 skill」—— 报错里能看到引号还挂在路径上。
|
||||
|
||||
只剥「成对」的引号("..." / '...' / “...” / 「...」);
|
||||
单侧引号不动 —— 半剥只会拼出一个更怪、照样不存在的路径,
|
||||
不如原样报错,用户一眼能看出是引号没复制全。
|
||||
"""
|
||||
s = (raw or "").strip()
|
||||
for a, b in _QUOTE_PAIRS:
|
||||
if len(s) >= 2 and s.startswith(a) and s.endswith(b):
|
||||
return s[1:-1].strip()
|
||||
return s
|
||||
|
||||
def probe_push(src: str, distro: str = DEFAULT_DISTRO, b64: bool = False) -> dict:
|
||||
"""推送前探查(只读,不写盘):校验 skill 规范 + 判断目标是否已有同名目录。
|
||||
|
||||
菜单层据此决定要不要问用户覆盖:
|
||||
exists=False -> 不存在同名,直接放(不该问覆盖,问了也是脱裤子放屁)
|
||||
exists=True -> 确有同名文件夹,才有资格问一次「是否覆盖」
|
||||
返回 {"ok", "err"/"name"/"dest"/"exists"}。
|
||||
"""
|
||||
src = clean_input(src)
|
||||
with tempfile.TemporaryDirectory(prefix="ocskill_probe_") as tmp:
|
||||
try:
|
||||
candidate = stage_candidate(src, Path(tmp), b64)
|
||||
except SystemExit as exc:
|
||||
return {"ok": False, "err": str(exc.code or exc)}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"ok": False, "err": f"输入解析失败:{exc}"}
|
||||
ok, err, meta = validate_skill(candidate)
|
||||
if not ok:
|
||||
return {"ok": False, "err": err}
|
||||
name = meta.get("name", "")
|
||||
dest = Path(wsl_skill_root(distro)) / name
|
||||
try:
|
||||
exists = dest.is_dir()
|
||||
except OSError:
|
||||
exists = False
|
||||
return {"ok": True, "name": name, "dest": str(dest), "exists": exists,
|
||||
"description": meta.get("description", "")}
|
||||
|
||||
def probe_pull(name: str, distro: str = DEFAULT_DISTRO) -> dict:
|
||||
"""拉取前探查(只读):WSL 端是否存在该 skill + 本机是否已有同名目录。"""
|
||||
name = clean_input(name)
|
||||
if not name:
|
||||
return {"ok": False, "err": "技能名为空"}
|
||||
if not NAME_RE.match(name):
|
||||
return {"ok": False, "err": f"非法 skill 名称「{name}」(只允许字母/数字/点/下划线/连字符,且以字母或数字开头)"}
|
||||
src = Path(wsl_skill_root(distro)) / name
|
||||
dest = Path(WIN_LOCAL_ROOT) / name
|
||||
try:
|
||||
src_ok = src.is_dir()
|
||||
except OSError:
|
||||
src_ok = False
|
||||
try:
|
||||
local_exists = dest.is_dir()
|
||||
except OSError:
|
||||
local_exists = False
|
||||
if not src_ok:
|
||||
return {"ok": False, "err": f"WSL 端不存在该 skill:{src}"}
|
||||
return {"ok": True, "name": name, "dest": str(dest), "exists": local_exists}
|
||||
|
||||
def probe_delete(name: str, distro: str = DEFAULT_DISTRO, local: bool = False) -> dict:
|
||||
"""删除前探查(只读):确认目标存在并给出将删除的路径。"""
|
||||
name = clean_input(name)
|
||||
if not name:
|
||||
return {"ok": False, "err": "技能名为空"}
|
||||
if not NAME_RE.match(name):
|
||||
return {"ok": False, "err": f"非法 skill 名称「{name}」(只允许字母/数字/点/下划线/连字符,且以字母或数字开头)"}
|
||||
root = Path(WIN_LOCAL_ROOT) if local else Path(wsl_skill_root(distro))
|
||||
dest = root / name
|
||||
try:
|
||||
ok = dest.is_dir()
|
||||
except OSError:
|
||||
ok = False
|
||||
if not ok:
|
||||
side = "本机" if local else "WSL"
|
||||
return {"ok": False, "err": f"{side}端不存在该 skill:{dest}"}
|
||||
return {"ok": True, "name": name, "dest": str(dest), "side": "本机" if local else "WSL"}
|
||||
|
||||
# ---------------------------------------------------------------- 标准 skill 校验
|
||||
NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||
|
||||
@@ -481,6 +576,7 @@ def decode_input(src: str, work: Path, b64: bool) -> Path | None:
|
||||
|
||||
def stage_candidate(src: str, work: Path, b64: bool) -> Path:
|
||||
"""归一化输入 -> 返回一个包含 SKILL.md 的候选目录。"""
|
||||
src = clean_input(src) # 剥掉粘贴时带上的引号("C:\..." -> C:\...)
|
||||
resolved = decode_input(src, work, b64)
|
||||
path = Path(resolved) if resolved else Path(os.path.expandvars(os.path.expanduser(src)))
|
||||
|
||||
@@ -657,14 +753,15 @@ def cmd_push(args) -> int:
|
||||
print(f"dry-run:不会写入 {dest}")
|
||||
return 0
|
||||
|
||||
# 探查式覆盖:没有同名文件夹就直接放(目录和内容都没有,覆盖无从谈起);
|
||||
# 确有同名才走 --force —— 「要不要覆盖」这一问只在真的存在时才发生。
|
||||
if dest.exists():
|
||||
if not args.force:
|
||||
raise SystemExit(
|
||||
f"目标已存在同名 skill:{dest}\n"
|
||||
f"加 --force 覆盖(覆盖前会自动备份为 .bak-<时间戳>)。"
|
||||
f"加 --force 覆盖(覆盖前会自动挪进 .trash/ 备份,可恢复)。"
|
||||
)
|
||||
backup = dest.with_name(dest.name + f".bak-{_stamp()}")
|
||||
shutil.move(str(dest), str(backup))
|
||||
backup = _backup_then_remove(dest)
|
||||
print(f"已备份旧版本 -> {backup}")
|
||||
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -679,23 +776,73 @@ def cmd_pull(args) -> int:
|
||||
gate_err = _wsl_ensure_running(args.distro, "pull")
|
||||
if gate_err:
|
||||
raise SystemExit(gate_err)
|
||||
name = clean_input(args.name) # 粘贴带引号也认
|
||||
if not name:
|
||||
raise SystemExit("技能名为空。")
|
||||
root = wsl_skill_root(args.distro)
|
||||
src = Path(root) / args.name
|
||||
src = Path(root) / name
|
||||
if not src.is_dir():
|
||||
raise SystemExit(f"WSL 端不存在该 skill:{src}")
|
||||
|
||||
out_dir = Path(args.out or ".")
|
||||
# 输出目录不由用户决定:默认直接落本机 OC 的 skill 目录(写完即被 watch 热加载)
|
||||
out_dir = Path(args.out) if args.out else Path(WIN_LOCAL_ROOT)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = out_dir / args.name
|
||||
dest = out_dir / name
|
||||
# 探查式覆盖:本机没有同名目录就直接放(不该问覆盖);有才需要 --force
|
||||
if dest.exists():
|
||||
if not args.force:
|
||||
raise SystemExit(f"本地已存在:{dest}(用 --force 覆盖)")
|
||||
shutil.rmtree(dest)
|
||||
raise SystemExit(
|
||||
f"本机已存在同名 skill:{dest}\n"
|
||||
f"加 --force 覆盖(覆盖前会自动挪进 .trash/ 备份,可恢复)。"
|
||||
)
|
||||
backup = _backup_then_remove(dest)
|
||||
print(f"已备份旧版本 -> {backup}")
|
||||
shutil.copytree(src, dest)
|
||||
print(f"已取回:{dest}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_delete(args) -> int:
|
||||
"""删除 skill(补齐管理闭环:列表 / 推送 / 拉取 / 删除 / 覆盖更新)。
|
||||
|
||||
缺省删 WSL 端(与 push 同侧),--local 删本机(与 pull 同侧)。
|
||||
删除绝不直接 rmtree:先挪进 <skills根>/.trash/<时间戳>/,删错了可恢复。
|
||||
--yes 跳过交互确认(菜单层已经问过用户时传入)。
|
||||
"""
|
||||
name = clean_input(args.name)
|
||||
if not name:
|
||||
raise SystemExit("技能名为空。")
|
||||
|
||||
# WSL 端是 UNC 访问:先唤醒再读,否则没跑的 VM 会被冷启动拖死
|
||||
if not args.local:
|
||||
gate_err = _wsl_ensure_running(args.distro, "delete")
|
||||
if gate_err:
|
||||
raise SystemExit(gate_err)
|
||||
|
||||
probe = probe_delete(name, args.distro, args.local)
|
||||
if not probe["ok"]:
|
||||
raise SystemExit(probe["err"])
|
||||
dest = Path(probe["dest"])
|
||||
side = probe["side"]
|
||||
|
||||
if not args.yes:
|
||||
try:
|
||||
ans = input(
|
||||
f"确认删除{side} skill「{name}」?\n"
|
||||
f" 路径:{dest}\n"
|
||||
f" (会先挪进 .trash/ 备份,可恢复)[y/N]: "
|
||||
).strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n已取消删除。")
|
||||
return 0
|
||||
if ans.lower() not in ("y", "yes", "是"):
|
||||
print("已取消删除。")
|
||||
return 0
|
||||
|
||||
backup = _backup_then_remove(dest)
|
||||
print(f"已删除{side} skill「{name}」(备份在 {backup},需要时可移回原位)")
|
||||
return 0
|
||||
|
||||
def cmd_dirs(args) -> int:
|
||||
print(f"WSL skill 目录(push 目标):{wsl_skill_root(args.distro)}")
|
||||
print(f"本机 skill 目录(pull 默认落到当前目录):{WIN_LOCAL_ROOT}")
|
||||
@@ -714,6 +861,22 @@ def _stamp() -> str:
|
||||
|
||||
return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
|
||||
def _backup_then_remove(dest: Path) -> Path:
|
||||
"""覆盖前把同名目录挪进 `<skills根>/.trash/<时间戳>/`,绝不直接 rmtree。
|
||||
|
||||
- 可恢复:覆盖错了还能从 .trash 里捞回来
|
||||
- 不污染:统一放点前缀 .trash/ 下,OC 的 skill loader 与 scan_skill_dir
|
||||
都跳过 `.` 开头的目录;旧的 `<name>.bak-<ts>` 平级备份会带 SKILL.md,
|
||||
反而可能被 OC 当成一个多余的 skill 加载出来
|
||||
"""
|
||||
trash = dest.parent / ".trash" / _stamp()
|
||||
trash.mkdir(parents=True, exist_ok=True)
|
||||
target = trash / dest.name
|
||||
if target.exists(): # 同秒内重复覆盖
|
||||
target = trash / f"{dest.name}-{_stamp()}"
|
||||
shutil.move(str(dest), str(target))
|
||||
return target
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 参数顺序自愈
|
||||
# 共享 flag 定义在主解析器(子命令之前),`ocskill list --distro xxx` 这种
|
||||
@@ -757,7 +920,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
" python ocskill.py push .\\my-skill\\SKILL.md\n"
|
||||
" python ocskill.py push <base64字符串> --base64\n"
|
||||
" python ocskill.py push .\\my-skill --dry-run\n"
|
||||
" python ocskill.py pull java-expert --out .\\skills-backup\n"
|
||||
" python ocskill.py pull java-expert # 直接落本机 OC skill 目录\n"
|
||||
" python ocskill.py delete java-expert # 删 WSL 端(先挪 .trash/)\n"
|
||||
" python ocskill.py delete java-expert --local # 删本机\n"
|
||||
),
|
||||
)
|
||||
p.add_argument("--distro", default=DEFAULT_DISTRO, help=f"WSL 发行版名(默认 {DEFAULT_DISTRO})")
|
||||
@@ -783,12 +948,18 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
sp.add_argument("--dry-run", action="store_true", help="只校验格式,不写入")
|
||||
sp.set_defaults(func=cmd_push)
|
||||
|
||||
sp = sub.add_parser("pull", help="把 WSL 里的 skill 拷回本机")
|
||||
sp.add_argument("name", help="skill 名称")
|
||||
sp.add_argument("--out", default=None, help="输出目录(默认当前目录)")
|
||||
sp.add_argument("--force", action="store_true", help="覆盖本地同名目录")
|
||||
sp = sub.add_parser("pull", help="把 WSL 里的 skill 拷回本机 OC skill 目录")
|
||||
sp.add_argument("name", help="skill 名称(可带引号,会自动剥掉)")
|
||||
sp.add_argument("--out", default=None, help=f"输出目录(默认本机 OC skill 目录:{WIN_LOCAL_ROOT})")
|
||||
sp.add_argument("--force", action="store_true", help="覆盖本机同名目录(先挪进 .trash/ 备份)")
|
||||
sp.set_defaults(func=cmd_pull)
|
||||
|
||||
sp = sub.add_parser("delete", help="删除 skill(先挪进 .trash/ 备份,可恢复)")
|
||||
sp.add_argument("name", help="skill 名称(可带引号,会自动剥掉)")
|
||||
sp.add_argument("--local", action="store_true", help="删本机的 skill(缺省删 WSL 端)")
|
||||
sp.add_argument("--yes", "-y", action="store_true", help="跳过交互确认(菜单层已确认时用)")
|
||||
sp.set_defaults(func=cmd_delete)
|
||||
|
||||
sp = sub.add_parser("dirs", help="显示两端 skill 目录与 API 连接状态")
|
||||
sp.set_defaults(func=cmd_dirs)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user