hw-cloudrobo-core 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. hw_cloudrobo_core-0.2.0/PKG-INFO +28 -0
  2. hw_cloudrobo_core-0.2.0/README.md +18 -0
  3. hw_cloudrobo_core-0.2.0/pyproject.toml +48 -0
  4. hw_cloudrobo_core-0.2.0/setup.cfg +4 -0
  5. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/__init__.py +0 -0
  6. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/__init__.py +0 -0
  7. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/cli_utils.py +15 -0
  8. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/config_cmd.py +173 -0
  9. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/config_utils.py +81 -0
  10. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/self_cmd.py +113 -0
  11. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/setup_user_config.py +36 -0
  12. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/skill_cmd.py +137 -0
  13. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/skill_loader.py +17 -0
  14. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli_main.py +219 -0
  15. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/config.yaml +18 -0
  16. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/models.py +7 -0
  17. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/plugins/__init__.py +49 -0
  18. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/__init__.py +4 -0
  19. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/apig_sdk_auth.py +134 -0
  20. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/auth.py +11 -0
  21. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/base_client.py +12 -0
  22. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/config.py +263 -0
  23. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/crypto.py +66 -0
  24. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/exceptions.py +46 -0
  25. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/http_client.py +433 -0
  26. hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/obs_client.py +502 -0
  27. hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/PKG-INFO +28 -0
  28. hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/SOURCES.txt +31 -0
  29. hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/dependency_links.txt +1 -0
  30. hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/entry_points.txt +2 -0
  31. hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/requires.txt +11 -0
  32. hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/top_level.txt +1 -0
  33. hw_cloudrobo_core-0.2.0/tests/test_sdk.py +64 -0
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: hw-cloudrobo-core
3
+ Version: 0.2.0
4
+ Summary: CloudRobo Core SDK and CLI Framework
5
+ Author-email: Huawei Cloud CloudRobo Team <hwcloudrobo@huawei.com>
6
+ License: Apache-2.0
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.8
19
+ Requires-Dist: click>=8.0
20
+ Requires-Dist: pyyaml>=5.0
21
+ Requires-Dist: requests>=2.25
22
+ Requires-Dist: rich>=10.0
23
+ Requires-Dist: cryptography>=41.0.0
24
+ Requires-Dist: esdk-obs-python
25
+ Requires-Dist: hw-cloudrobo-workspace>=0.1.0
26
+ Requires-Dist: hw-cloudrobo-resource>=0.1.0
27
+ Provides-Extra: obs
28
+ Requires-Dist: pycryptodome==3.10.1; extra == "obs"
@@ -0,0 +1,18 @@
1
+ # cloudrobo-core
2
+
3
+ 核心 SDK 与 CLI 框架,提供 Config、HttpClient、BaseClient 和 CLI 插件加载机制。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install -e packages/cloudrobo-core
9
+ ```
10
+
11
+ ## 文档
12
+
13
+ - [模块概览](docs/index.md)
14
+ - [CLI 命令](docs/commands.md)
15
+ - [使用示例](docs/examples.md)
16
+ - [开发指南](docs/development.md)
17
+ - [安装指南](../../docs/installation.md)
18
+ - [架构文档](../../docs/architecture.md)
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools==83.0.0", "packaging==26.2", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hw-cloudrobo-core"
7
+ version = "0.2.0"
8
+ description = "CloudRobo Core SDK and CLI Framework"
9
+ requires-python = ">=3.8"
10
+ authors = [{name = "Huawei Cloud CloudRobo Team", email = "hwcloudrobo@huawei.com"}]
11
+ license = {text = "Apache-2.0"}
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "click>=8.0",
27
+ "pyyaml>=5.0",
28
+ "requests>=2.25",
29
+ "rich>=10.0",
30
+ "cryptography>=41.0.0",
31
+ "esdk-obs-python",
32
+ "hw-cloudrobo-workspace>=0.1.0",
33
+ "hw-cloudrobo-resource>=0.1.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ obs = [
38
+ "pycryptodome==3.10.1",
39
+ ]
40
+
41
+ [project.scripts]
42
+ cloudrobo = "cloudrobo_core.cli_main:main"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
46
+
47
+ [tool.setuptools.package-data]
48
+ cloudrobo_core = ["config.yaml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,15 @@
1
+ import json
2
+ import os
3
+ import click
4
+ from cloudrobo_core.sdk import Config, HttpClient
5
+
6
+
7
+ def get_client(ctx, client_class):
8
+ config_path = os.environ.get("CLOUDROBO_SERVICE_CONFIG")
9
+ config = Config(config_path or None)
10
+ http = HttpClient(config)
11
+ return client_class(http)
12
+
13
+
14
+ def out(result):
15
+ click.echo(json.dumps(result, ensure_ascii=False, indent=2))
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python
2
+ import os
3
+
4
+ import click
5
+ import yaml
6
+
7
+ from cloudrobo_core.cli.config_utils import USER_CONFIG_PATH
8
+
9
+ _SENSITIVE_KEYS = {"ak", "sk"}
10
+
11
+
12
+ def _mask(value: str) -> str:
13
+ if len(value) > 8:
14
+ return value[:4] + "****" + value[-4:]
15
+ return "****"
16
+
17
+
18
+ @click.group()
19
+ def config():
20
+ """配置管理命令"""
21
+ pass
22
+
23
+
24
+ @config.command("set")
25
+ @click.argument("pairs", nargs=-1, required=True)
26
+ def set_config(pairs):
27
+ """设置配置项(支持一次设置多个)
28
+
29
+ 用法:
30
+ cloudrobo config set ak xxx sk yyy
31
+ cloudrobo config set ak xxx sk yyy region cn-southwest-2
32
+
33
+ 支持的 key:
34
+ ak, sk, region
35
+
36
+ ak/sk 会自动加密存储(机器绑定),region 明文存储。
37
+ """
38
+ if len(pairs) % 2 != 0:
39
+ click.echo("参数必须是 key-value 对,例如: config set ak xxx sk yyy", err=True)
40
+ return
41
+
42
+ config_path = USER_CONFIG_PATH
43
+
44
+ data = {}
45
+ if config_path.exists():
46
+ with open(config_path, "r", encoding="utf-8") as f:
47
+ data = yaml.safe_load(f) or {}
48
+
49
+ if "cloudrobo" not in data:
50
+ data["cloudrobo"] = {}
51
+ if "auth" not in data["cloudrobo"]:
52
+ data["cloudrobo"]["auth"] = {}
53
+
54
+ key_map = {
55
+ "ak": ("cloudrobo", "auth", "ak"),
56
+ "sk": ("cloudrobo", "auth", "sk"),
57
+ "region": ("cloudrobo", "region"),
58
+ }
59
+
60
+ for i in range(0, len(pairs), 2):
61
+ key, value = pairs[i], pairs[i + 1]
62
+
63
+ if key not in key_map:
64
+ click.echo(f"不支持的配置项: {key}", err=True)
65
+ click.echo(f"支持的配置项: {', '.join(key_map.keys())}", err=True)
66
+ return
67
+
68
+ if key in _SENSITIVE_KEYS:
69
+ from cloudrobo_core.sdk.crypto import encrypt
70
+ enc_value = encrypt(value)
71
+ data["cloudrobo"]["auth"][f"{key}_enc"] = enc_value
72
+ data["cloudrobo"]["auth"].pop(key, None)
73
+ click.echo(f"已设置 {key} (加密存储)")
74
+ else:
75
+ path = key_map[key]
76
+ obj = data
77
+ for p in path[:-1]:
78
+ if p not in obj:
79
+ obj[p] = {}
80
+ obj = obj[p]
81
+ obj[path[-1]] = value
82
+ click.echo(f"已设置 {key} = {value}")
83
+
84
+ config_path.parent.mkdir(parents=True, exist_ok=True)
85
+ with open(config_path, "w", encoding="utf-8") as f:
86
+ yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
87
+ try:
88
+ os.chmod(config_path, 0o600)
89
+ except OSError:
90
+ pass
91
+
92
+
93
+ @config.command("get")
94
+ @click.argument("key")
95
+ def get_config(key):
96
+ """获取配置项
97
+
98
+ 支持的 key:
99
+ ak, sk, region
100
+
101
+ ak/sk 解密后脱敏显示(仅显示前4后4位)。
102
+ """
103
+ config_path = USER_CONFIG_PATH
104
+
105
+ if not config_path.exists():
106
+ click.echo(f"配置文件不存在: {config_path}", err=True)
107
+ return
108
+
109
+ with open(config_path, "r", encoding="utf-8") as f:
110
+ data = yaml.safe_load(f) or {}
111
+
112
+ cloudrobo = data.get("cloudrobo", {})
113
+ auth = cloudrobo.get("auth", {})
114
+
115
+ if key in _SENSITIVE_KEYS:
116
+ enc_field = f"{key}_enc"
117
+ enc_value = auth.get(enc_field)
118
+ if enc_value:
119
+ from cloudrobo_core.sdk.crypto import decrypt
120
+ try:
121
+ value = decrypt(enc_value)
122
+ click.echo(_mask(value))
123
+ except Exception:
124
+ click.echo("(解密失败)", err=True)
125
+ else:
126
+ plain = auth.get(key, "")
127
+ if plain:
128
+ click.echo(f"{_mask(plain)} (明文存储)")
129
+ else:
130
+ click.echo(f"{key} 未设置")
131
+ return
132
+
133
+ if key == "region":
134
+ value = cloudrobo.get("region", "")
135
+ click.echo(value if value else f"{key} 未设置")
136
+ return
137
+
138
+ click.echo(f"不支持的配置项: {key}", err=True)
139
+ click.echo(f"支持的配置项: ak, sk, region", err=True)
140
+
141
+
142
+ @config.command("list")
143
+ def list_config():
144
+ """列出所有配置"""
145
+ config_path = USER_CONFIG_PATH
146
+
147
+ if not config_path.exists():
148
+ click.echo(f"配置文件不存在: {config_path}", err=True)
149
+ return
150
+
151
+ with open(config_path, "r", encoding="utf-8") as f:
152
+ data = yaml.safe_load(f) or {}
153
+
154
+ cloudrobo = data.get("cloudrobo", {})
155
+ auth = cloudrobo.get("auth", {})
156
+
157
+ click.echo(f"配置文件: {config_path}")
158
+ click.echo()
159
+ click.echo("认证配置:")
160
+
161
+ for key in ("ak", "sk"):
162
+ enc_field = f"{key}_enc"
163
+ if auth.get(enc_field):
164
+ click.echo(f" {key}: 已加密存储 ✓")
165
+ elif auth.get(key):
166
+ click.echo(f" {key}: 明文存储 ⚠ (建议重新配置以启用加密存储)")
167
+ else:
168
+ click.echo(f" {key}: (未设置)")
169
+
170
+ click.echo()
171
+ click.echo("其他配置:")
172
+ region = cloudrobo.get("region", "")
173
+ click.echo(f" region: {region if region else '(未设置)'}")
@@ -0,0 +1,81 @@
1
+ import logging
2
+ import os
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ import yaml
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ USER_CONFIG_DIR = Path.home() / ".cloudrobo"
12
+ USER_CONFIG_PATH = USER_CONFIG_DIR / "config.yaml"
13
+
14
+ _FIELDS_TO_CLEAR = [
15
+ ("cloudrobo", "auth", "ak"),
16
+ ("cloudrobo", "auth", "sk"),
17
+ ("cloudrobo", "proxy", "http"),
18
+ ("cloudrobo", "proxy", "https"),
19
+ ("cloudrobo", "proxy", "no_proxy"),
20
+ ("debug", "ca_bundle"),
21
+ ]
22
+
23
+
24
+ def _load_project_config() -> Dict[str, Any]:
25
+ try:
26
+ config_file = files("cloudrobo_core").joinpath("config.yaml")
27
+ with open(str(config_file), "r", encoding="utf-8") as f:
28
+ return yaml.safe_load(f) or {}
29
+ except (FileNotFoundError, yaml.YAMLError) as e:
30
+ logger.debug("Failed to load project config via importlib: %s", e)
31
+ fallback = Path(__file__).resolve()
32
+ for _ in range(6):
33
+ fallback = fallback.parent
34
+ candidate = fallback / "config.yaml"
35
+ if candidate.exists():
36
+ try:
37
+ with open(candidate, "r", encoding="utf-8") as f:
38
+ return yaml.safe_load(f) or {}
39
+ except (FileNotFoundError, yaml.YAMLError) as e:
40
+ logger.debug("Failed to load fallback config %s: %s", candidate, e)
41
+ continue
42
+ return {}
43
+
44
+
45
+ def _generate_user_template() -> str:
46
+ data = _load_project_config()
47
+ for path in _FIELDS_TO_CLEAR:
48
+ node = data
49
+ for key in path[:-1]:
50
+ if isinstance(node, dict) and key in node:
51
+ node = node[key]
52
+ else:
53
+ node = None
54
+ break
55
+ if node is not None and isinstance(node, dict):
56
+ node[path[-1]] = ""
57
+ header = "# CloudRobo 用户配置(优先级高于工程目录 config/config.yaml)\n"
58
+ header += "# 在此填入你的 AK/SK,无需修改工程目录下的配置\n\n"
59
+ return header + yaml.dump(data, allow_unicode=True, default_flow_style=False)
60
+
61
+
62
+ def ensure_user_config() -> Path:
63
+ if not USER_CONFIG_PATH.exists():
64
+ USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
65
+ USER_CONFIG_PATH.write_text(_generate_user_template(), encoding="utf-8")
66
+ try:
67
+ os.chmod(USER_CONFIG_PATH, 0o600)
68
+ except OSError:
69
+ pass
70
+ logger.info("Created user config template: %s", USER_CONFIG_PATH)
71
+ return USER_CONFIG_PATH
72
+
73
+
74
+ def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
75
+ result = dict(base)
76
+ for k, v in override.items():
77
+ if k in result and isinstance(result[k], dict) and isinstance(v, dict):
78
+ result[k] = deep_merge(result[k], v)
79
+ else:
80
+ result[k] = v
81
+ return result
@@ -0,0 +1,113 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import tempfile
5
+
6
+ import click
7
+
8
+
9
+ @click.group()
10
+ def self():
11
+ """CloudRobo 自身管理命令组"""
12
+ pass
13
+
14
+
15
+ @self.command("uninstall")
16
+ @click.argument("packages", nargs=-1, required=False)
17
+ @click.option("--yes", "-y", is_flag=True, help="跳过确认提示")
18
+ @click.option("--all", "uninstall_all", is_flag=True, help="卸载所有 CloudRobo 包")
19
+ def uninstall(packages, yes, uninstall_all):
20
+ """卸载 CloudRobo Client 安装包
21
+
22
+ 不指定包名时卸载所有包。支持子包简写(如 asset, dataset)或完整包名。
23
+
24
+ 示例:
25
+
26
+ cloudrobo self uninstall # 卸载所有包
27
+
28
+ cloudrobo self uninstall asset # 卸载 asset 子包
29
+
30
+ cloudrobo self uninstall hw-cloudrobo-asset # 卸载 asset 子包(完整名)
31
+
32
+ cloudrobo self uninstall asset dataset # 卸载多个子包
33
+ """
34
+ # 获取已安装的包列表
35
+ result = subprocess.run(
36
+ [sys.executable, "-m", "pip", "list", "--format=columns"],
37
+ capture_output=True,
38
+ text=True,
39
+ )
40
+ if result.returncode != 0:
41
+ click.echo(f"获取安装包列表失败: {result.stderr.strip()}", err=True)
42
+ sys.exit(1)
43
+
44
+ # 扫描所有 cloudrobo 相关包(支持开发模式和发布模式)
45
+ installed_packages = []
46
+ for line in result.stdout.splitlines():
47
+ parts = line.split()
48
+ if parts and (
49
+ parts[0].startswith("cloudrobo-")
50
+ or parts[0].startswith("hw-cloudrobo-")
51
+ ):
52
+ installed_packages.append(parts[0])
53
+
54
+ if not installed_packages:
55
+ click.echo("未找到任何 CloudRobo 安装包。")
56
+ return
57
+
58
+ # 确定要卸载的包
59
+ if uninstall_all or not packages:
60
+ # 卸载所有包
61
+ to_uninstall = installed_packages
62
+ else:
63
+ # 卸载指定包(支持简写和完整名)
64
+ to_uninstall = []
65
+ for pkg in packages:
66
+ # 如果已经是完整包名且已安装
67
+ if pkg in installed_packages:
68
+ to_uninstall.append(pkg)
69
+ else:
70
+ # 尝试匹配简写(asset → hw-cloudrobo-asset 或 cloudrobo-asset)
71
+ matched = [
72
+ p for p in installed_packages
73
+ if p.endswith(f"-{pkg}") or p == f"hw-cloudrobo-{pkg}" or p == f"cloudrobo-{pkg}"
74
+ ]
75
+ if matched:
76
+ to_uninstall.extend(matched)
77
+ else:
78
+ click.echo(f"警告: 未找到包 '{pkg}',跳过。")
79
+
80
+ if not to_uninstall:
81
+ click.echo("没有需要卸载的包。")
82
+ return
83
+
84
+ click.echo("即将卸载以下安装包:")
85
+ for pkg in to_uninstall:
86
+ click.echo(f" - {pkg}")
87
+
88
+ if not yes:
89
+ if not click.confirm("确认卸载?"):
90
+ click.echo("已取消。")
91
+ return
92
+
93
+ # 生成卸载脚本(避免自杀问题)
94
+ script = "import subprocess, sys\n"
95
+ script += f"pkgs = {to_uninstall!r}\n"
96
+ script += "cmd = [sys.executable, '-m', 'pip', 'uninstall', '-y'] + pkgs\n"
97
+ script += "r = subprocess.run(cmd)\n"
98
+ script += "sys.exit(r.returncode)\n"
99
+
100
+ script_path = os.path.join(tempfile.gettempdir(), "cloudrobo_uninstall.py")
101
+ with open(script_path, "w", encoding="utf-8") as f:
102
+ f.write(script)
103
+
104
+ click.echo("卸载脚本已生成,即将退出当前进程并执行卸载...")
105
+ popen_kwargs = {}
106
+ if sys.platform == "win32":
107
+ popen_kwargs["creationflags"] = (
108
+ subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW
109
+ )
110
+ else:
111
+ popen_kwargs["start_new_session"] = True
112
+ subprocess.Popen([sys.executable, script_path], **popen_kwargs)
113
+ sys.exit(0)
@@ -0,0 +1,36 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ USER_CLOUDROBO_DIR = Path.home() / ".cloudrobo"
8
+ USER_CONFIG_PATH = USER_CLOUDROBO_DIR / "config.yaml"
9
+
10
+ _DEFAULT_USER_CONFIG = """\
11
+ # CloudRobo 用户配置(优先级高于工程目录 config/config.yaml)
12
+ # 在此填入你的 AK/SK,无需修改工程目录下的配置
13
+
14
+ cloudrobo:
15
+ auth:
16
+ ak: ""
17
+ sk: ""
18
+ """
19
+
20
+
21
+ def ensure_user_config() -> Path:
22
+ USER_CLOUDROBO_DIR.mkdir(parents=True, exist_ok=True)
23
+ if not USER_CONFIG_PATH.exists():
24
+ USER_CONFIG_PATH.write_text(_DEFAULT_USER_CONFIG, encoding="utf-8")
25
+ logger.info("Created user config: %s", USER_CONFIG_PATH)
26
+ return USER_CONFIG_PATH
27
+
28
+
29
+ def _post_install():
30
+ ensure_user_config()
31
+ secrets_dir = USER_CLOUDROBO_DIR / "conversations"
32
+ secrets_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ _post_install()
@@ -0,0 +1,137 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from .skill_loader import _parse_frontmatter
8
+
9
+
10
+ TARGET_MAP = {
11
+ "claude-code": "~/.claude/skills",
12
+ "claude-code-project": ".claude/skills",
13
+ "jiuwenswarm": "~/.jiuwenswarm/agent/workspace/skills",
14
+ }
15
+
16
+
17
+ def _resolve_target(target: str) -> Path:
18
+ if target.startswith("path:"):
19
+ return Path(target[5:]).expanduser().resolve()
20
+
21
+ if target not in TARGET_MAP:
22
+ click.echo(f"Unknown target: {target}", err=True)
23
+ click.echo(f"Supported targets: {', '.join(TARGET_MAP.keys())}, path:/custom/dir", err=True)
24
+ return None
25
+
26
+ if target == "jiuwenswarm":
27
+ data_dir = os.environ.get("JIUWENSWARM_DATA_DIR")
28
+ if data_dir:
29
+ return Path(data_dir) / "agent" / "workspace" / "skills"
30
+
31
+ return Path(TARGET_MAP[target]).expanduser().resolve()
32
+
33
+
34
+ def _list_skills_from(skills_root: Path) -> list:
35
+ skills = []
36
+ for skill_dir in sorted(skills_root.iterdir()):
37
+ if not skill_dir.is_dir():
38
+ continue
39
+ skill_md = skill_dir / "SKILL.md"
40
+ if not skill_md.exists():
41
+ continue
42
+ meta = _parse_frontmatter(skill_md)
43
+ skills.append({
44
+ "name": meta.get("name", skill_dir.name),
45
+ "description": meta.get("description", ""),
46
+ "dir": str(skill_dir),
47
+ })
48
+ return skills
49
+
50
+
51
+ @click.group(name="skill")
52
+ def skill():
53
+ """Skill management commands"""
54
+ pass
55
+
56
+
57
+ @skill.command("install")
58
+ @click.option("--target", required=True,
59
+ help="Target platform (claude-code, claude-code-project, jiuwenswarm) or custom path (path:/dir)")
60
+ @click.option("--source", required=True,
61
+ help="Skill source directory")
62
+ @click.option("--skill-name", default=None,
63
+ help="Specific skills to install (comma-separated). Default: all skills")
64
+ def install(target, source, skill_name):
65
+ """Install skills to agent platform directories"""
66
+ target_dir = _resolve_target(target)
67
+ if target_dir is None:
68
+ return
69
+
70
+ target_dir = Path(target_dir).expanduser().resolve()
71
+ target_dir.mkdir(parents=True, exist_ok=True)
72
+
73
+ skills_root = Path(source).expanduser().resolve()
74
+ if not skills_root.exists():
75
+ click.echo(f"Source directory not found: {skills_root}", err=True)
76
+ return
77
+ skills = _list_skills_from(skills_root)
78
+
79
+ if skill_name:
80
+ skill_names = [n.strip() for n in skill_name.split(",") if n.strip()]
81
+ skills = [s for s in skills if s["name"] in skill_names]
82
+ missing = set(skill_names) - {s["name"] for s in skills}
83
+ if missing:
84
+ click.echo(f"Skills not found: {', '.join(missing)}", err=True)
85
+
86
+ if not skills:
87
+ click.echo("No matching skills to install.")
88
+ return
89
+
90
+ installed = []
91
+ for s in skills:
92
+ src = Path(s["dir"])
93
+ dst = target_dir / src.name
94
+ if dst.exists():
95
+ shutil.rmtree(dst)
96
+ shutil.copytree(src, dst)
97
+ installed.append(s["name"])
98
+
99
+ click.echo(f"Installed {len(installed)} skill(s) to {target_dir}:")
100
+ for name in installed:
101
+ click.echo(f" {name}")
102
+
103
+
104
+ @skill.command("uninstall")
105
+ @click.option("--target", required=True,
106
+ help="Target platform (claude-code, claude-code-project, jiuwenswarm) or custom path (path:/dir)")
107
+ @click.option("--skill-name", default=None,
108
+ help="Specific skills to uninstall (comma-separated). Default: all skills")
109
+ def uninstall(target, skill_name):
110
+ """Uninstall skills from agent platform directories"""
111
+ target_dir = _resolve_target(target)
112
+ if target_dir is None:
113
+ return
114
+
115
+ target_dir = Path(target_dir).expanduser().resolve()
116
+ if not target_dir.exists():
117
+ click.echo(f"Target directory does not exist: {target_dir}")
118
+ return
119
+
120
+ if skill_name:
121
+ skills_to_remove = [n.strip() for n in skill_name.split(",") if n.strip()]
122
+ else:
123
+ skills_to_remove = [d.name for d in target_dir.iterdir() if d.is_dir()]
124
+
125
+ removed = []
126
+ for name in skills_to_remove:
127
+ skill_path = target_dir / name
128
+ if skill_path.exists():
129
+ shutil.rmtree(skill_path)
130
+ removed.append(name)
131
+
132
+ if removed:
133
+ click.echo(f"Uninstalled {len(removed)} skill(s) from {target_dir}:")
134
+ for name in removed:
135
+ click.echo(f" {name}")
136
+ else:
137
+ click.echo("No skills to uninstall.")
@@ -0,0 +1,17 @@
1
+ """Skill loader: SKILL.md frontmatter parsing."""
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Dict
5
+
6
+ import yaml
7
+
8
+
9
+ def _parse_frontmatter(path: Path) -> Dict:
10
+ text = path.read_text(encoding="utf-8")
11
+ m = re.match(r"^---[ \t]*\r?\n(.*?)\r?\n---", text, re.DOTALL)
12
+ if not m:
13
+ return {}
14
+ try:
15
+ return yaml.safe_load(m.group(1)) or {}
16
+ except yaml.YAMLError:
17
+ return {}