kitup-sdk 0.1.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.
- kitup_sdk-0.1.2/.gitignore +16 -0
- kitup_sdk-0.1.2/PKG-INFO +50 -0
- kitup_sdk-0.1.2/README.md +38 -0
- kitup_sdk-0.1.2/pyproject.toml +34 -0
- kitup_sdk-0.1.2/src/kitup/__init__.py +95 -0
- kitup_sdk-0.1.2/src/kitup/_github.py +107 -0
- kitup_sdk-0.1.2/src/kitup/_hosts_generated.py +3 -0
- kitup_sdk-0.1.2/src/kitup/_metadata.py +46 -0
- kitup_sdk-0.1.2/src/kitup/_paths.py +40 -0
- kitup_sdk-0.1.2/src/kitup/bundle.py +177 -0
- kitup_sdk-0.1.2/src/kitup/hosts.py +102 -0
- kitup_sdk-0.1.2/src/kitup/install.py +384 -0
- kitup_sdk-0.1.2/src/kitup/types.py +224 -0
- kitup_sdk-0.1.2/src/kitup/workflow.py +639 -0
- kitup_sdk-0.1.2/tests/golden_test.py +536 -0
- kitup_sdk-0.1.2/tests/test_bundle.py +174 -0
- kitup_sdk-0.1.2/tests/test_hosts.py +103 -0
- kitup_sdk-0.1.2/tests/test_install.py +382 -0
- kitup_sdk-0.1.2/tests/test_workflow.py +512 -0
kitup_sdk-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kitup-sdk
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Shared installer SDK for bundled Agent Skills.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: agent,installer,sdk,skills
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
10
|
+
Requires-Dist: ruff>=0.12.0; extra == 'dev'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# kitup Python SDK
|
|
14
|
+
|
|
15
|
+
Shared installer SDK for bundled Agent Skills.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install kitup-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
Use the workflow API for user-facing install commands:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from kitup import (
|
|
29
|
+
BaseOptions,
|
|
30
|
+
InstallOptions,
|
|
31
|
+
InstallWorkflowOptions,
|
|
32
|
+
directory_bundle,
|
|
33
|
+
run_bundled_skill_install,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
workflow = run_bundled_skill_install(
|
|
37
|
+
InstallWorkflowOptions(
|
|
38
|
+
install=InstallOptions(
|
|
39
|
+
base=BaseOptions(),
|
|
40
|
+
app_id="mycli",
|
|
41
|
+
skill_bundle=directory_bundle("./skills/mycli"),
|
|
42
|
+
scope="user",
|
|
43
|
+
),
|
|
44
|
+
stdin_tty=True,
|
|
45
|
+
prompt_scope=True,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Call `install_bundled_skill` when your CLI already knows the target scope and agents and does not need the interactive workflow surface.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# kitup Python SDK
|
|
2
|
+
|
|
3
|
+
Shared installer SDK for bundled Agent Skills.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kitup-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
Use the workflow API for user-facing install commands:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from kitup import (
|
|
17
|
+
BaseOptions,
|
|
18
|
+
InstallOptions,
|
|
19
|
+
InstallWorkflowOptions,
|
|
20
|
+
directory_bundle,
|
|
21
|
+
run_bundled_skill_install,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
workflow = run_bundled_skill_install(
|
|
25
|
+
InstallWorkflowOptions(
|
|
26
|
+
install=InstallOptions(
|
|
27
|
+
base=BaseOptions(),
|
|
28
|
+
app_id="mycli",
|
|
29
|
+
skill_bundle=directory_bundle("./skills/mycli"),
|
|
30
|
+
scope="user",
|
|
31
|
+
),
|
|
32
|
+
stdin_tty=True,
|
|
33
|
+
prompt_scope=True,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Call `install_bundled_skill` when your CLI already knows the target scope and agents and does not need the interactive workflow surface.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kitup-sdk"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Shared installer SDK for bundled Agent Skills."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["agent", "skills", "installer", "sdk"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.3.0",
|
|
18
|
+
"ruff>=0.12.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.3.0",
|
|
24
|
+
"ruff>=0.12.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/kitup"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
target-version = "py314"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from .bundle import (
|
|
2
|
+
compute_bundle_content_hash,
|
|
3
|
+
directory_bundle,
|
|
4
|
+
files_bundle,
|
|
5
|
+
github_bundle,
|
|
6
|
+
validate_skill_bundle,
|
|
7
|
+
)
|
|
8
|
+
from .hosts import detect_hosts, load_host_spec, resolve_hosts
|
|
9
|
+
from .install import (
|
|
10
|
+
install_bundled_skill,
|
|
11
|
+
plan_bundled_skill,
|
|
12
|
+
resolve_install_targets,
|
|
13
|
+
uninstall_bundled_skill,
|
|
14
|
+
update_bundled_skill,
|
|
15
|
+
)
|
|
16
|
+
from .workflow import (
|
|
17
|
+
agent_selector_from_flags,
|
|
18
|
+
classify_install_workflow_exit,
|
|
19
|
+
install_flag_error,
|
|
20
|
+
install_workflow_error,
|
|
21
|
+
parse_install_flags,
|
|
22
|
+
parse_scope_flag,
|
|
23
|
+
resolve_install_selection,
|
|
24
|
+
run_bundled_skill_install,
|
|
25
|
+
run_bundled_skill_install_with_io,
|
|
26
|
+
)
|
|
27
|
+
from .types import (
|
|
28
|
+
BaseOptions,
|
|
29
|
+
GitHubBundleOptions,
|
|
30
|
+
Host,
|
|
31
|
+
HostSpec,
|
|
32
|
+
INSTALL_UX,
|
|
33
|
+
InstallOptions,
|
|
34
|
+
InstallReport,
|
|
35
|
+
InstallSelection,
|
|
36
|
+
InstallSelectionOptions,
|
|
37
|
+
InstallWorkflowExit,
|
|
38
|
+
InstallWorkflowOptions,
|
|
39
|
+
InstallWorkflowReport,
|
|
40
|
+
KitupError,
|
|
41
|
+
ParsedInstallFlags,
|
|
42
|
+
SkillFile,
|
|
43
|
+
TargetError,
|
|
44
|
+
TargetGroup,
|
|
45
|
+
TargetResult,
|
|
46
|
+
TargetStatus,
|
|
47
|
+
UninstallOptions,
|
|
48
|
+
UninstallReport,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"BaseOptions",
|
|
53
|
+
"GitHubBundleOptions",
|
|
54
|
+
"Host",
|
|
55
|
+
"HostSpec",
|
|
56
|
+
"INSTALL_UX",
|
|
57
|
+
"InstallOptions",
|
|
58
|
+
"InstallReport",
|
|
59
|
+
"InstallSelection",
|
|
60
|
+
"InstallSelectionOptions",
|
|
61
|
+
"InstallWorkflowExit",
|
|
62
|
+
"InstallWorkflowOptions",
|
|
63
|
+
"InstallWorkflowReport",
|
|
64
|
+
"KitupError",
|
|
65
|
+
"ParsedInstallFlags",
|
|
66
|
+
"SkillFile",
|
|
67
|
+
"TargetError",
|
|
68
|
+
"TargetGroup",
|
|
69
|
+
"TargetResult",
|
|
70
|
+
"TargetStatus",
|
|
71
|
+
"UninstallOptions",
|
|
72
|
+
"UninstallReport",
|
|
73
|
+
"compute_bundle_content_hash",
|
|
74
|
+
"detect_hosts",
|
|
75
|
+
"directory_bundle",
|
|
76
|
+
"files_bundle",
|
|
77
|
+
"github_bundle",
|
|
78
|
+
"agent_selector_from_flags",
|
|
79
|
+
"classify_install_workflow_exit",
|
|
80
|
+
"install_bundled_skill",
|
|
81
|
+
"install_flag_error",
|
|
82
|
+
"install_workflow_error",
|
|
83
|
+
"load_host_spec",
|
|
84
|
+
"parse_install_flags",
|
|
85
|
+
"parse_scope_flag",
|
|
86
|
+
"plan_bundled_skill",
|
|
87
|
+
"resolve_hosts",
|
|
88
|
+
"resolve_install_selection",
|
|
89
|
+
"resolve_install_targets",
|
|
90
|
+
"run_bundled_skill_install",
|
|
91
|
+
"run_bundled_skill_install_with_io",
|
|
92
|
+
"uninstall_bundled_skill",
|
|
93
|
+
"update_bundled_skill",
|
|
94
|
+
"validate_skill_bundle",
|
|
95
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
from ._paths import trim_github_path
|
|
9
|
+
from .types import GitHubBundleOptions, KitupError, SkillFile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fetch_github_directory(options: GitHubBundleOptions) -> list[SkillFile]:
|
|
13
|
+
files, _ = fetch_github_directory_with_metadata(options)
|
|
14
|
+
return files
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch_github_directory_with_metadata(
|
|
18
|
+
options: GitHubBundleOptions,
|
|
19
|
+
) -> tuple[list[SkillFile], dict[str, object]]:
|
|
20
|
+
root = trim_github_path(options.path)
|
|
21
|
+
if not options.owner or not options.repo or not root or not options.ref:
|
|
22
|
+
raise KitupError("invalid github bundle")
|
|
23
|
+
|
|
24
|
+
api_base = _env_base_url("KITUP_GITHUB_API_BASE_URL", "https://api.github.com")
|
|
25
|
+
raw_base = _env_base_url(
|
|
26
|
+
"KITUP_GITHUB_RAW_BASE_URL",
|
|
27
|
+
"https://raw.githubusercontent.com",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
commit = github_json(
|
|
31
|
+
f"{api_base}/repos/{_encode_path_part(options.owner)}/"
|
|
32
|
+
f"{_encode_path_part(options.repo)}/commits/{_encode_path_part(options.ref)}"
|
|
33
|
+
)
|
|
34
|
+
resolved_commit = str(commit.get("sha") or "")
|
|
35
|
+
tree_sha = str(((commit.get("commit") or {}).get("tree") or {}).get("sha") or "")
|
|
36
|
+
if not resolved_commit or not tree_sha:
|
|
37
|
+
raise KitupError("invalid github commit")
|
|
38
|
+
|
|
39
|
+
tree = github_json(
|
|
40
|
+
f"{api_base}/repos/{_encode_path_part(options.owner)}/"
|
|
41
|
+
f"{_encode_path_part(options.repo)}/git/trees/{_encode_path_part(tree_sha)}"
|
|
42
|
+
"?recursive=1"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
prefix = f"{root}/"
|
|
46
|
+
files: list[SkillFile] = []
|
|
47
|
+
for item in tree.get("tree") or []:
|
|
48
|
+
if not isinstance(item, dict):
|
|
49
|
+
continue
|
|
50
|
+
path = str(item.get("path") or "")
|
|
51
|
+
if item.get("type") != "blob" or not path.startswith(prefix):
|
|
52
|
+
continue
|
|
53
|
+
url = (
|
|
54
|
+
f"{raw_base}/{_encode_path_part(options.owner)}/"
|
|
55
|
+
f"{_encode_path_part(options.repo)}/"
|
|
56
|
+
f"{_encode_path_part(resolved_commit)}/{_encode_path(path)}"
|
|
57
|
+
)
|
|
58
|
+
files.append(
|
|
59
|
+
SkillFile(
|
|
60
|
+
path=path[len(prefix) :],
|
|
61
|
+
contents=github_bytes(url),
|
|
62
|
+
mode=0o755 if item.get("mode") == "100755" else 0o644,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not files:
|
|
67
|
+
raise KitupError("github bundle path not found")
|
|
68
|
+
|
|
69
|
+
return files, {
|
|
70
|
+
"source": "github",
|
|
71
|
+
"source_id": f"github:{options.owner}/{options.repo}/{root}",
|
|
72
|
+
"version": options.ref,
|
|
73
|
+
"provenance": {
|
|
74
|
+
"owner": options.owner,
|
|
75
|
+
"repo": options.repo,
|
|
76
|
+
"path": root,
|
|
77
|
+
"ref": options.ref,
|
|
78
|
+
"resolvedCommit": resolved_commit,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def github_json(url: str) -> dict[str, object]:
|
|
84
|
+
with urllib.request.urlopen(_request(url), timeout=30) as response:
|
|
85
|
+
return json.loads(response.read().decode("utf-8"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def github_bytes(url: str) -> bytes:
|
|
89
|
+
with urllib.request.urlopen(_request(url), timeout=30) as response:
|
|
90
|
+
return response.read()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _request(url: str) -> urllib.request.Request:
|
|
94
|
+
return urllib.request.Request(url, headers={"User-Agent": "kitup"})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _env_base_url(name: str, fallback: str) -> str:
|
|
98
|
+
value = os.environ.get(name, "").rstrip("/")
|
|
99
|
+
return value or fallback
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _encode_path(path: str) -> str:
|
|
103
|
+
return "/".join(_encode_path_part(part) for part in path.split("/"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _encode_path_part(part: str) -> str:
|
|
107
|
+
return urllib.parse.quote(part, safe="")
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Code generated from spec/hosts.json. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
DEFAULT_HOSTS_SPEC_JSON = "{\"$schema\":\"./hosts.schema.json\",\"schemaVersion\":1,\"hosts\":[{\"id\":\"adal\",\"displayName\":\"AdaL\",\"projectSkillsDirs\":[\".adal/skills\"],\"userSkillsDirs\":[\"~/.adal/skills\"],\"detect\":[\"~/.adal\"],\"status\":\"community\"},{\"id\":\"aider-desk\",\"displayName\":\"AiderDesk\",\"projectSkillsDirs\":[\".aider-desk/skills\"],\"userSkillsDirs\":[\"~/.aider-desk/skills\"],\"detect\":[\"~/.aider-desk\"],\"status\":\"community\"},{\"id\":\"amp\",\"displayName\":\"Amp\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.config/agents/skills\"],\"detect\":[\"~/.config/amp\",\"~/.config/agents\"],\"status\":\"community\"},{\"id\":\"antigravity\",\"displayName\":\"Antigravity\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.gemini/antigravity/skills\"],\"detect\":[\"~/.gemini/antigravity\"],\"status\":\"community\"},{\"id\":\"antigravity-cli\",\"displayName\":\"Antigravity CLI\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.gemini/antigravity-cli/skills\"],\"detect\":[\"~/.gemini/antigravity-cli\"],\"status\":\"community\"},{\"id\":\"astrbot\",\"displayName\":\"AstrBot\",\"projectSkillsDirs\":[\"data/skills\"],\"userSkillsDirs\":[\"~/.astrbot/data/skills\"],\"detect\":[\"~/.astrbot\",\"data/skills\",\"~/.astrbot/data\"],\"status\":\"community\"},{\"id\":\"augment\",\"displayName\":\"Augment\",\"projectSkillsDirs\":[\".augment/skills\"],\"userSkillsDirs\":[\"~/.augment/skills\"],\"detect\":[\"~/.augment\"],\"status\":\"community\"},{\"id\":\"autohand-code\",\"displayName\":\"Autohand Code CLI\",\"projectSkillsDirs\":[\".autohand/skills\"],\"userSkillsDirs\":[\"~/.autohand/skills\"],\"detect\":[\"~/.autohand\"],\"status\":\"community\"},{\"id\":\"bob\",\"displayName\":\"IBM Bob\",\"projectSkillsDirs\":[\".bob/skills\"],\"userSkillsDirs\":[\"~/.bob/skills\"],\"detect\":[\"~/.bob\"],\"status\":\"community\"},{\"id\":\"claude-code\",\"displayName\":\"Claude Code\",\"projectSkillsDirs\":[\".claude/skills\"],\"userSkillsDirs\":[\"~/.claude/skills\"],\"detect\":[\"~/.claude\"],\"status\":\"verified\"},{\"id\":\"cline\",\"displayName\":\"Cline\",\"projectSkillsDirs\":[\".agents/skills\",\".cline/skills\",\".clinerules/skills\",\".claude/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.cline/skills\"],\"detect\":[\"~/.cline\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"codearts-agent\",\"displayName\":\"CodeArts Agent\",\"projectSkillsDirs\":[\".codeartsdoer/skills\"],\"userSkillsDirs\":[\"~/.codeartsdoer/skills\"],\"detect\":[\"~/.codeartsdoer\"],\"status\":\"community\"},{\"id\":\"codebuddy\",\"displayName\":\"CodeBuddy\",\"projectSkillsDirs\":[\".codebuddy/skills\"],\"userSkillsDirs\":[\"~/.codebuddy/skills\"],\"detect\":[\"~/.codebuddy\",\".codebuddy\"],\"status\":\"community\"},{\"id\":\"codemaker\",\"displayName\":\"Codemaker\",\"projectSkillsDirs\":[\".codemaker/skills\"],\"userSkillsDirs\":[\"~/.codemaker/skills\"],\"detect\":[\"~/.codemaker\"],\"status\":\"community\"},{\"id\":\"codestudio\",\"displayName\":\"Code Studio\",\"projectSkillsDirs\":[\".codestudio/skills\"],\"userSkillsDirs\":[\"~/.codestudio/skills\"],\"detect\":[\"~/.codestudio\"],\"status\":\"community\"},{\"id\":\"codex\",\"displayName\":\"Codex\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.codex/skills\"],\"detect\":[\"~/.codex\",\"~/.agents/skills\",\"~/.agents\"],\"status\":\"verified\",\"notes\":[\"Keep both ~/.agents/skills and ~/.codex/skills for compatibility.\"]},{\"id\":\"command-code\",\"displayName\":\"Command Code\",\"projectSkillsDirs\":[\".commandcode/skills\"],\"userSkillsDirs\":[\"~/.commandcode/skills\"],\"detect\":[\"~/.commandcode\"],\"status\":\"community\"},{\"id\":\"continue\",\"displayName\":\"Continue\",\"projectSkillsDirs\":[\".continue/skills\"],\"userSkillsDirs\":[\"~/.continue/skills\"],\"detect\":[\"~/.continue\",\".continue\"],\"status\":\"community\"},{\"id\":\"cortex\",\"displayName\":\"Cortex Code\",\"projectSkillsDirs\":[\".cortex/skills\"],\"userSkillsDirs\":[\"~/.snowflake/cortex/skills\"],\"detect\":[\"~/.snowflake/cortex\"],\"status\":\"community\"},{\"id\":\"crush\",\"displayName\":\"Crush\",\"projectSkillsDirs\":[\".crush/skills\"],\"userSkillsDirs\":[\"~/.config/crush/skills\"],\"detect\":[\"~/.config/crush\"],\"status\":\"community\"},{\"id\":\"cursor\",\"displayName\":\"Cursor\",\"projectSkillsDirs\":[\".agents/skills\",\".cursor/skills\"],\"userSkillsDirs\":[\"~/.cursor/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.cursor\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"deepagents\",\"displayName\":\"Deep Agents\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.deepagents/agent/skills\"],\"detect\":[\"~/.deepagents\",\"~/.deepagents/agent\"],\"status\":\"community\"},{\"id\":\"devin\",\"displayName\":\"Devin for Terminal\",\"projectSkillsDirs\":[\".devin/skills\"],\"userSkillsDirs\":[\"~/.config/devin/skills\"],\"detect\":[\"~/.config/devin\"],\"status\":\"community\"},{\"id\":\"dexto\",\"displayName\":\"Dexto\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\"],\"detect\":[\"~/.dexto\",\"~/.agents\"],\"status\":\"community\"},{\"id\":\"droid\",\"displayName\":\"Droid\",\"projectSkillsDirs\":[\".factory/skills\"],\"userSkillsDirs\":[\"~/.factory/skills\"],\"detect\":[\"~/.factory\"],\"status\":\"community\"},{\"id\":\"eve\",\"displayName\":\"Eve\",\"projectSkillsDirs\":[\"agent/skills\"],\"userSkillsDirs\":[],\"detect\":[\"agent\",\"package.json\"],\"status\":\"community\",\"notes\":[\"Project-only host; userSkillsDirs is intentionally empty.\",\"Detect from Eve project shape; no global skill directory.\"]},{\"id\":\"firebender\",\"displayName\":\"Firebender\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.firebender/skills\"],\"detect\":[\"~/.firebender\"],\"status\":\"community\"},{\"id\":\"forgecode\",\"displayName\":\"ForgeCode\",\"projectSkillsDirs\":[\".forge/skills\"],\"userSkillsDirs\":[\"~/.forge/skills\"],\"detect\":[\"~/.forge\"],\"status\":\"community\"},{\"id\":\"gemini-cli\",\"displayName\":\"Gemini CLI\",\"projectSkillsDirs\":[\".agents/skills\",\".gemini/skills\"],\"userSkillsDirs\":[\"~/.gemini/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.gemini\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"github-copilot\",\"displayName\":\"GitHub Copilot\",\"projectSkillsDirs\":[\".agents/skills\",\".github/skills\",\".claude/skills\"],\"userSkillsDirs\":[\"~/.copilot/skills\",\"~/.agents/skills\",\"~/.claude/skills\"],\"detect\":[\"~/.copilot\",\"~/.agents\",\"~/.claude\"],\"status\":\"documented\"},{\"id\":\"goose\",\"displayName\":\"Goose\",\"projectSkillsDirs\":[\".goose/skills\"],\"userSkillsDirs\":[\"~/.config/goose/skills\"],\"detect\":[\"~/.config/goose\"],\"status\":\"community\"},{\"id\":\"hermes-agent\",\"displayName\":\"Hermes Agent\",\"projectSkillsDirs\":[\".hermes/skills\"],\"userSkillsDirs\":[\"~/.hermes/skills\"],\"detect\":[\"~/.hermes\"],\"status\":\"community\"},{\"id\":\"iflow-cli\",\"displayName\":\"iFlow CLI\",\"projectSkillsDirs\":[\".iflow/skills\"],\"userSkillsDirs\":[\"~/.iflow/skills\"],\"detect\":[\"~/.iflow\"],\"status\":\"community\"},{\"id\":\"inference-sh\",\"displayName\":\"inference.sh\",\"projectSkillsDirs\":[\".inferencesh/skills\"],\"userSkillsDirs\":[\"~/.inferencesh/skills\"],\"detect\":[\"~/.inferencesh\"],\"status\":\"community\"},{\"id\":\"jazz\",\"displayName\":\"Jazz\",\"projectSkillsDirs\":[\".jazz/skills\"],\"userSkillsDirs\":[\"~/.jazz/skills\"],\"detect\":[\"~/.jazz\",\".jazz\"],\"status\":\"community\"},{\"id\":\"junie\",\"displayName\":\"Junie\",\"projectSkillsDirs\":[\".junie/skills\"],\"userSkillsDirs\":[\"~/.junie/skills\"],\"detect\":[\"~/.junie\"],\"status\":\"community\"},{\"id\":\"kilo\",\"displayName\":\"Kilo Code\",\"projectSkillsDirs\":[\".kilocode/skills\"],\"userSkillsDirs\":[\"~/.kilocode/skills\"],\"detect\":[\"~/.kilocode\"],\"status\":\"community\"},{\"id\":\"kimi-cli\",\"displayName\":\"Kimi Code CLI\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.config/agents/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.config/agents\",\"~/.kimi-code\",\"~/.kimi\",\"~/.agents\"],\"status\":\"community\",\"notes\":[\"kimi-code-cli is an alias for the same Kimi Code CLI path family.\"],\"aliases\":[\"kimi-code-cli\"]},{\"id\":\"kiro-cli\",\"displayName\":\"Kiro CLI\",\"projectSkillsDirs\":[\".kiro/skills\"],\"userSkillsDirs\":[\"~/.kiro/skills\"],\"detect\":[\"~/.kiro\"],\"status\":\"community\"},{\"id\":\"kode\",\"displayName\":\"Kode\",\"projectSkillsDirs\":[\".kode/skills\"],\"userSkillsDirs\":[\"~/.kode/skills\"],\"detect\":[\"~/.kode\"],\"status\":\"community\"},{\"id\":\"lingma\",\"displayName\":\"Lingma\",\"projectSkillsDirs\":[\".lingma/skills\"],\"userSkillsDirs\":[\"~/.lingma/skills\"],\"detect\":[\"~/.lingma\"],\"status\":\"community\"},{\"id\":\"loaf\",\"displayName\":\"Loaf\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\"],\"detect\":[\"~/.loaf\",\"~/.agents\"],\"status\":\"community\"},{\"id\":\"mcpjam\",\"displayName\":\"MCPJam\",\"projectSkillsDirs\":[\".mcpjam/skills\"],\"userSkillsDirs\":[\"~/.mcpjam/skills\"],\"detect\":[\"~/.mcpjam\"],\"status\":\"community\"},{\"id\":\"mistral-vibe\",\"displayName\":\"Mistral Vibe\",\"projectSkillsDirs\":[\".vibe/skills\"],\"userSkillsDirs\":[\"~/.vibe/skills\"],\"detect\":[\"~/.vibe\"],\"status\":\"community\"},{\"id\":\"moxby\",\"displayName\":\"Moxby\",\"projectSkillsDirs\":[\".moxby/skills\"],\"userSkillsDirs\":[\"~/.moxby/skills\"],\"detect\":[\"~/.moxby\"],\"status\":\"community\"},{\"id\":\"mux\",\"displayName\":\"Mux\",\"projectSkillsDirs\":[\".mux/skills\"],\"userSkillsDirs\":[\"~/.mux/skills\"],\"detect\":[\"~/.mux\"],\"status\":\"community\"},{\"id\":\"neovate\",\"displayName\":\"Neovate\",\"projectSkillsDirs\":[\".neovate/skills\"],\"userSkillsDirs\":[\"~/.neovate/skills\"],\"detect\":[\"~/.neovate\"],\"status\":\"community\"},{\"id\":\"ona\",\"displayName\":\"Ona\",\"projectSkillsDirs\":[\".ona/skills\"],\"userSkillsDirs\":[\"~/.ona/skills\"],\"detect\":[\"~/.ona\"],\"status\":\"community\"},{\"id\":\"openclaw\",\"displayName\":\"OpenClaw\",\"projectSkillsDirs\":[\"skills\"],\"userSkillsDirs\":[\"~/.openclaw/skills\"],\"detect\":[\"~/.openclaw\",\"~/.clawdbot\",\"~/.moltbot\"],\"status\":\"community\"},{\"id\":\"opencode\",\"displayName\":\"OpenCode\",\"projectSkillsDirs\":[\".agents/skills\",\".opencode/skills\",\".claude/skills\"],\"userSkillsDirs\":[\"~/.config/opencode/skills\",\"~/.agents/skills\",\"~/.claude/skills\"],\"detect\":[\"~/.config/opencode\",\"~/.agents\",\"~/.claude\"],\"status\":\"verified\"},{\"id\":\"openhands\",\"displayName\":\"OpenHands\",\"projectSkillsDirs\":[\".openhands/skills\"],\"userSkillsDirs\":[\"~/.openhands/skills\"],\"detect\":[\"~/.openhands\"],\"status\":\"community\"},{\"id\":\"pi\",\"displayName\":\"Pi\",\"projectSkillsDirs\":[\".pi/skills\"],\"userSkillsDirs\":[\"~/.pi/agent/skills\"],\"detect\":[\"~/.pi/agent\"],\"status\":\"community\"},{\"id\":\"pochi\",\"displayName\":\"Pochi\",\"projectSkillsDirs\":[\".pochi/skills\"],\"userSkillsDirs\":[\"~/.pochi/skills\"],\"detect\":[\"~/.pochi\"],\"status\":\"community\"},{\"id\":\"promptscript\",\"displayName\":\"PromptScript\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[],\"detect\":[\".promptscript\",\"promptscript.yaml\"],\"status\":\"community\",\"notes\":[\"Project-only host; userSkillsDirs is intentionally empty.\"]},{\"id\":\"qoder\",\"displayName\":\"Qoder\",\"projectSkillsDirs\":[\".qoder/skills\"],\"userSkillsDirs\":[\"~/.qoder/skills\"],\"detect\":[\"~/.qoder\"],\"status\":\"community\"},{\"id\":\"qoder-cn\",\"displayName\":\"Qoder CN\",\"projectSkillsDirs\":[\".qoder/skills\"],\"userSkillsDirs\":[\"~/.qoder-cn/skills\"],\"detect\":[\"~/.qoder-cn\"],\"status\":\"community\"},{\"id\":\"qwen-code\",\"displayName\":\"Qwen Code\",\"projectSkillsDirs\":[\".qwen/skills\"],\"userSkillsDirs\":[\"~/.qwen/skills\"],\"detect\":[\"~/.qwen\"],\"status\":\"community\"},{\"id\":\"reasonix\",\"displayName\":\"Reasonix\",\"projectSkillsDirs\":[\".reasonix/skills\"],\"userSkillsDirs\":[\"~/.reasonix/skills\"],\"detect\":[\"~/.reasonix\"],\"status\":\"community\"},{\"id\":\"replit\",\"displayName\":\"Replit\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.config/agents/skills\"],\"detect\":[\".replit\",\"~/.config/agents\"],\"status\":\"community\"},{\"id\":\"roo\",\"displayName\":\"Roo Code\",\"aliases\":[\"roo-code\"],\"projectSkillsDirs\":[\".roo/skills\",\".agents/skills\"],\"userSkillsDirs\":[\"~/.roo/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.roo\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"rovodev\",\"displayName\":\"Rovo Dev\",\"projectSkillsDirs\":[\".rovodev/skills\"],\"userSkillsDirs\":[\"~/.rovodev/skills\"],\"detect\":[\"~/.rovodev\"],\"status\":\"community\"},{\"id\":\"tabnine-cli\",\"displayName\":\"Tabnine CLI\",\"projectSkillsDirs\":[\".tabnine/agent/skills\"],\"userSkillsDirs\":[\"~/.tabnine/agent/skills\"],\"detect\":[\"~/.tabnine\",\"~/.tabnine/agent\"],\"status\":\"community\"},{\"id\":\"terramind\",\"displayName\":\"Terramind\",\"projectSkillsDirs\":[\".terramind/skills\"],\"userSkillsDirs\":[\"~/.terramind/skills\"],\"detect\":[\"~/.terramind\"],\"status\":\"community\"},{\"id\":\"tinycloud\",\"displayName\":\"Tinycloud\",\"projectSkillsDirs\":[\".tinycloud/skills\"],\"userSkillsDirs\":[\"~/.tinycloud/skills\"],\"detect\":[\"~/.tinycloud\"],\"status\":\"community\"},{\"id\":\"trae\",\"displayName\":\"Trae\",\"projectSkillsDirs\":[\".trae/skills\"],\"userSkillsDirs\":[\"~/.trae/skills\"],\"detect\":[\"~/.trae\"],\"status\":\"community\"},{\"id\":\"trae-cn\",\"displayName\":\"Trae CN\",\"projectSkillsDirs\":[\".trae/skills\"],\"userSkillsDirs\":[\"~/.trae-cn/skills\"],\"detect\":[\"~/.trae-cn\"],\"status\":\"community\"},{\"id\":\"universal\",\"displayName\":\"Universal\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.config/agents/skills\"],\"detect\":[\"~/.agents\",\"~/.config/agents\"],\"status\":\"community\"},{\"id\":\"warp\",\"displayName\":\"Warp\",\"projectSkillsDirs\":[\".agents/skills\",\".warp/skills\",\".claude/skills\",\".codex/skills\",\".cursor/skills\",\".gemini/skills\",\".copilot/skills\",\".factory/skills\",\".github/skills\",\".opencode/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.warp/skills\",\"~/.claude/skills\",\"~/.codex/skills\",\"~/.cursor/skills\",\"~/.gemini/skills\",\"~/.copilot/skills\",\"~/.factory/skills\",\"~/.github/skills\",\"~/.opencode/skills\"],\"detect\":[\"~/.warp\",\"~/.agents\",\"~/.claude\",\"~/.codex\",\"~/.cursor\",\"~/.gemini\",\"~/.copilot\",\"~/.factory\",\"~/.github\",\"~/.opencode\"],\"status\":\"documented\"},{\"id\":\"windsurf\",\"displayName\":\"Windsurf\",\"projectSkillsDirs\":[\".windsurf/skills\"],\"userSkillsDirs\":[\"~/.codeium/windsurf/skills\"],\"detect\":[\"~/.codeium/windsurf\"],\"status\":\"community\"},{\"id\":\"zed\",\"displayName\":\"Zed\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\"],\"detect\":[\"~/.config/zed\",\"~/.agents\"],\"status\":\"community\"},{\"id\":\"zencoder\",\"displayName\":\"Zencoder\",\"projectSkillsDirs\":[\".zencoder/skills\"],\"userSkillsDirs\":[\"~/.zencoder/skills\"],\"detect\":[\"~/.zencoder\"],\"status\":\"community\"},{\"id\":\"zenflow\",\"displayName\":\"Zenflow\",\"projectSkillsDirs\":[\".zencoder/skills\"],\"userSkillsDirs\":[\"~/.zencoder/skills\"],\"detect\":[\"~/.zencoder\"],\"status\":\"community\"}]}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def write_install_metadata(
|
|
8
|
+
target_dir: Path,
|
|
9
|
+
*,
|
|
10
|
+
app_id: str,
|
|
11
|
+
skill_name: str,
|
|
12
|
+
digest: str,
|
|
13
|
+
source: str,
|
|
14
|
+
source_id: str | None = None,
|
|
15
|
+
version: str | None = None,
|
|
16
|
+
provenance: dict[str, object] | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
payload = {
|
|
19
|
+
"schemaVersion": 1,
|
|
20
|
+
"appId": app_id,
|
|
21
|
+
"skillName": skill_name,
|
|
22
|
+
"source": source,
|
|
23
|
+
"hash": digest,
|
|
24
|
+
}
|
|
25
|
+
if source_id is not None:
|
|
26
|
+
payload["sourceId"] = source_id
|
|
27
|
+
if version is not None:
|
|
28
|
+
payload["version"] = version
|
|
29
|
+
if provenance is not None:
|
|
30
|
+
payload["provenance"] = provenance
|
|
31
|
+
(target_dir / ".kitup.json").write_text(
|
|
32
|
+
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_install_metadata(target_dir: Path) -> dict[str, object] | None:
|
|
37
|
+
metadata_file = target_dir / ".kitup.json"
|
|
38
|
+
if not metadata_file.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
payload = json.loads(metadata_file.read_text(encoding="utf-8"))
|
|
42
|
+
except OSError, ValueError:
|
|
43
|
+
return None
|
|
44
|
+
if not isinstance(payload, dict):
|
|
45
|
+
return None
|
|
46
|
+
return payload
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .types import KitupError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def skip_name(name: str) -> bool:
|
|
9
|
+
return (
|
|
10
|
+
name == ".git"
|
|
11
|
+
or name == ".kitup.json"
|
|
12
|
+
or name == ".DS_Store"
|
|
13
|
+
or name.endswith(".swp")
|
|
14
|
+
or name.endswith("~")
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_bundle_path(value: str) -> str | None:
|
|
19
|
+
if not value or "\\" in value or value.startswith("/") or value[1:2] == ":":
|
|
20
|
+
raise KitupError(f"invalid skill file path: {value}")
|
|
21
|
+
|
|
22
|
+
parts = value.split("/")
|
|
23
|
+
for part in parts:
|
|
24
|
+
if not part or part in {".", ".."}:
|
|
25
|
+
raise KitupError(f"invalid skill file path: {value}")
|
|
26
|
+
if skip_name(part):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
return "/".join(parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_path(path: str, *, cwd: str | None = None) -> Path:
|
|
33
|
+
resolved = Path(path)
|
|
34
|
+
if not resolved.is_absolute() and cwd is not None:
|
|
35
|
+
resolved = Path(cwd) / resolved
|
|
36
|
+
return resolved
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def trim_github_path(path: str) -> str:
|
|
40
|
+
return path.strip("/")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ._github import fetch_github_directory
|
|
10
|
+
from ._paths import normalize_bundle_path, resolve_path, skip_name
|
|
11
|
+
from .types import (
|
|
12
|
+
BundleFile,
|
|
13
|
+
GitHubBundleOptions,
|
|
14
|
+
KitupError,
|
|
15
|
+
NormalizedSkillBundle,
|
|
16
|
+
SkillFile,
|
|
17
|
+
SkillInfo,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class DirectoryBundle:
|
|
23
|
+
path: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FilesBundle:
|
|
28
|
+
files: list[SkillFile]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class GitHubBundle:
|
|
33
|
+
options: GitHubBundleOptions
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
SkillBundle = DirectoryBundle | FilesBundle | GitHubBundle
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def directory_bundle(path: str) -> DirectoryBundle:
|
|
40
|
+
return DirectoryBundle(path=path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def files_bundle(files: list[SkillFile]) -> FilesBundle:
|
|
44
|
+
return FilesBundle(files=files)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def github_bundle(options: GitHubBundleOptions) -> GitHubBundle:
|
|
48
|
+
return GitHubBundle(options=options)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_skill_bundle(bundle: SkillBundle, cwd: str | None = None) -> SkillInfo:
|
|
52
|
+
try:
|
|
53
|
+
normalized = normalize_skill_bundle(bundle, cwd=cwd)
|
|
54
|
+
except Exception:
|
|
55
|
+
return SkillInfo(valid=False, error_code="invalid-skill-bundle")
|
|
56
|
+
|
|
57
|
+
return validate_normalized_skill_bundle(normalized)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_normalized_skill_bundle(normalized: NormalizedSkillBundle) -> SkillInfo:
|
|
61
|
+
skill_md = normalized.by_path.get("SKILL.md")
|
|
62
|
+
if skill_md is None:
|
|
63
|
+
return SkillInfo(valid=False, error_code="missing-skill-md")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
content = skill_md.bytes.decode("utf-8")
|
|
67
|
+
except UnicodeDecodeError:
|
|
68
|
+
return SkillInfo(valid=False, error_code="invalid-frontmatter")
|
|
69
|
+
|
|
70
|
+
match = re.match(r"^---\r?\n([\s\S]*?)\r?\n---\r?\n", content)
|
|
71
|
+
if match is None:
|
|
72
|
+
return SkillInfo(valid=False, error_code="invalid-frontmatter")
|
|
73
|
+
|
|
74
|
+
fields = _parse_frontmatter(match.group(1))
|
|
75
|
+
skill_name = fields.get("name", "")
|
|
76
|
+
description = fields.get("description", "")
|
|
77
|
+
if not _valid_skill_name(skill_name) or not description or len(description) > 1024:
|
|
78
|
+
return SkillInfo(valid=False, error_code="invalid-frontmatter")
|
|
79
|
+
|
|
80
|
+
return SkillInfo(valid=True, skill_name=skill_name, description=description)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_bundle_content_hash(bundle: SkillBundle, cwd: str | None = None) -> str:
|
|
84
|
+
normalized = normalize_skill_bundle(bundle, cwd=cwd)
|
|
85
|
+
return compute_normalized_bundle_content_hash(normalized)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def compute_normalized_bundle_content_hash(normalized: NormalizedSkillBundle) -> str:
|
|
89
|
+
digest = hashlib.sha256()
|
|
90
|
+
for file in normalized.files:
|
|
91
|
+
digest.update(file.path.encode("utf-8"))
|
|
92
|
+
digest.update(b"\0")
|
|
93
|
+
digest.update(file.bytes)
|
|
94
|
+
digest.update(b"\0")
|
|
95
|
+
return f"sha256:{digest.hexdigest()}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def normalize_skill_bundle(
|
|
99
|
+
bundle: SkillBundle, cwd: str | None = None
|
|
100
|
+
) -> NormalizedSkillBundle:
|
|
101
|
+
if isinstance(bundle, DirectoryBundle):
|
|
102
|
+
return normalize_directory_bundle(bundle.path, cwd=cwd)
|
|
103
|
+
if isinstance(bundle, FilesBundle):
|
|
104
|
+
return normalize_files_bundle(bundle.files)
|
|
105
|
+
if isinstance(bundle, GitHubBundle):
|
|
106
|
+
return normalize_files_bundle(fetch_github_directory(bundle.options))
|
|
107
|
+
raise KitupError(f"unsupported bundle: {type(bundle)!r}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def normalize_directory_bundle(
|
|
111
|
+
path: str, cwd: str | None = None
|
|
112
|
+
) -> NormalizedSkillBundle:
|
|
113
|
+
root = resolve_path(path, cwd=cwd)
|
|
114
|
+
if not root.is_dir():
|
|
115
|
+
raise KitupError(f"invalid bundle directory: {root}")
|
|
116
|
+
files: list[SkillFile] = []
|
|
117
|
+
|
|
118
|
+
for current_root, dirnames, filenames in os.walk(root):
|
|
119
|
+
dirnames[:] = sorted(name for name in dirnames if not skip_name(name))
|
|
120
|
+
for filename in sorted(filenames):
|
|
121
|
+
if skip_name(filename):
|
|
122
|
+
continue
|
|
123
|
+
source = Path(current_root) / filename
|
|
124
|
+
relative = source.relative_to(root).as_posix()
|
|
125
|
+
files.append(
|
|
126
|
+
SkillFile(
|
|
127
|
+
path=relative,
|
|
128
|
+
contents=source.read_bytes(),
|
|
129
|
+
mode=source.stat().st_mode & 0o777,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return normalize_files_bundle(files)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def normalize_files_bundle(files: list[SkillFile]) -> NormalizedSkillBundle:
|
|
137
|
+
by_path: dict[str, BundleFile] = {}
|
|
138
|
+
for file in files:
|
|
139
|
+
normalized_path = normalize_bundle_path(file.path)
|
|
140
|
+
if normalized_path is None:
|
|
141
|
+
continue
|
|
142
|
+
if normalized_path in by_path:
|
|
143
|
+
raise KitupError(f"duplicate skill file: {normalized_path}")
|
|
144
|
+
by_path[normalized_path] = BundleFile(
|
|
145
|
+
path=normalized_path,
|
|
146
|
+
bytes=file.contents.encode("utf-8")
|
|
147
|
+
if isinstance(file.contents, str)
|
|
148
|
+
else file.contents,
|
|
149
|
+
mode=file.mode or 0o644,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
normalized_files = [by_path[path] for path in sorted(by_path)]
|
|
153
|
+
return NormalizedSkillBundle(files=normalized_files, by_path=by_path)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def copy_normalized_bundle(files: list[BundleFile], target_dir: str | Path) -> None:
|
|
157
|
+
destination_root = Path(target_dir)
|
|
158
|
+
destination_root.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
for file in files:
|
|
160
|
+
destination = destination_root / file.path
|
|
161
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
destination.write_bytes(file.bytes)
|
|
163
|
+
destination.chmod(file.mode)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _parse_frontmatter(content: str) -> dict[str, str]:
|
|
167
|
+
fields: dict[str, str] = {}
|
|
168
|
+
for line in content.splitlines():
|
|
169
|
+
if ":" not in line:
|
|
170
|
+
continue
|
|
171
|
+
key, value = line.split(":", 1)
|
|
172
|
+
fields[key] = value.strip()
|
|
173
|
+
return fields
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _valid_skill_name(name: str) -> bool:
|
|
177
|
+
return re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) is not None
|