s-skillkit 0.1.2__tar.gz → 0.1.3__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.
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/PKG-INFO +1 -1
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/pyproject.toml +48 -48
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/installer.py +35 -9
- s_skillkit-0.1.3/tests/test_w268_subdir_skill.py +111 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/uv.lock +1 -1
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/.gitignore +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/LICENSE +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/README.md +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/__init__.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/collections.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/deps_installer.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/filter.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/linker.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/manifest.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/mcp_register.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/path_store.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/paths.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/project.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/targets/__init__.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/targets/antigravity.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/targets/base.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/targets/claude_code.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/targets/codex.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/targets/detect.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/skillkit/tooling.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/conftest.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_collections.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_deps_installer.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_filter.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_git_install.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_installer.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_manifest.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_mcp_register.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_path_store.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_paths.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_project.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_stub_guard.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_targets.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_tooling.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_traversal_guard.py +0 -0
- {s_skillkit-0.1.2 → s_skillkit-0.1.3}/tests/test_w272_empty_manifest_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s-skillkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs.
|
|
5
5
|
Author: Dmitry
|
|
6
6
|
License: MIT
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "s-skillkit"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs."
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
license = { text = "MIT" }
|
|
7
|
-
requires-python = ">=3.11"
|
|
8
|
-
authors = [{ name = "Dmitry" }]
|
|
9
|
-
# skillkit — SIBLING librarykit (НЕ зависит от него, httpx, auth, сети).
|
|
10
|
-
# Только локальная ФС-механика навыков. Core deps:
|
|
11
|
-
# tomli-w — запись TOML (project-манифест .skills-hub/skills.toml,
|
|
12
|
-
# _skill_meta.toml ридеры используют stdlib tomllib на чтение);
|
|
13
|
-
# pathspec — gitignore-стиль .skillignore / files-allowlist фильтр;
|
|
14
|
-
# platformdirs — нативные дефолтные пути (Paths.default) по ОС.
|
|
15
|
-
dependencies = [
|
|
16
|
-
"tomli-w>=1.0",
|
|
17
|
-
"pathspec>=0.12.0",
|
|
18
|
-
"platformdirs>=4.0",
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
[project.optional-dependencies]
|
|
22
|
-
dev = ["pytest>=8", "pytest-asyncio>=0.24"]
|
|
23
|
-
|
|
24
|
-
[build-system]
|
|
25
|
-
requires = ["hatchling"]
|
|
26
|
-
build-backend = "hatchling.build"
|
|
27
|
-
|
|
28
|
-
[tool.hatch.build.targets.wheel]
|
|
29
|
-
packages = ["skillkit"]
|
|
30
|
-
|
|
31
|
-
[tool.ruff]
|
|
32
|
-
line-length = 100
|
|
33
|
-
target-version = "py311"
|
|
34
|
-
src = ["."]
|
|
35
|
-
|
|
36
|
-
[tool.ruff.lint]
|
|
37
|
-
select = ["E", "F", "I", "B", "UP", "N", "SIM", "RUF"]
|
|
38
|
-
# RUF001/RUF002/RUF003 — кириллица в строках/докстрингах/комментах намеренная
|
|
39
|
-
# (русскоязычный код), ruff принимает её за «двусмысленную» латиницу.
|
|
40
|
-
# RUF022 — __all__ сгруппирован по модулям с комментами осознанно.
|
|
41
|
-
# RUF059 — распаковка кортежа в тестах с неиспользуемой частью (фикстура
|
|
42
|
-
# возвращает (store, target, store_dir); читаемее, чем индексирование).
|
|
43
|
-
ignore = ["RUF001", "RUF002", "RUF003", "RUF022", "RUF059"]
|
|
44
|
-
|
|
45
|
-
[tool.pytest.ini_options]
|
|
46
|
-
asyncio_mode = "auto"
|
|
47
|
-
testpaths = ["tests"]
|
|
48
|
-
pythonpath = ["."]
|
|
1
|
+
[project]
|
|
2
|
+
name = "s-skillkit"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
description = "Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Dmitry" }]
|
|
9
|
+
# skillkit — SIBLING librarykit (НЕ зависит от него, httpx, auth, сети).
|
|
10
|
+
# Только локальная ФС-механика навыков. Core deps:
|
|
11
|
+
# tomli-w — запись TOML (project-манифест .skills-hub/skills.toml,
|
|
12
|
+
# _skill_meta.toml ридеры используют stdlib tomllib на чтение);
|
|
13
|
+
# pathspec — gitignore-стиль .skillignore / files-allowlist фильтр;
|
|
14
|
+
# platformdirs — нативные дефолтные пути (Paths.default) по ОС.
|
|
15
|
+
dependencies = [
|
|
16
|
+
"tomli-w>=1.0",
|
|
17
|
+
"pathspec>=0.12.0",
|
|
18
|
+
"platformdirs>=4.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24"]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["skillkit"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 100
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
src = ["."]
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "B", "UP", "N", "SIM", "RUF"]
|
|
38
|
+
# RUF001/RUF002/RUF003 — кириллица в строках/докстрингах/комментах намеренная
|
|
39
|
+
# (русскоязычный код), ruff принимает её за «двусмысленную» латиницу.
|
|
40
|
+
# RUF022 — __all__ сгруппирован по модулям с комментами осознанно.
|
|
41
|
+
# RUF059 — распаковка кортежа в тестах с неиспользуемой частью (фикстура
|
|
42
|
+
# возвращает (store, target, store_dir); читаемее, чем индексирование).
|
|
43
|
+
ignore = ["RUF001", "RUF002", "RUF003", "RUF022", "RUF059"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
asyncio_mode = "auto"
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
pythonpath = ["."]
|
|
@@ -409,6 +409,7 @@ class SkillStore:
|
|
|
409
409
|
skill_id: str | int | None = None,
|
|
410
410
|
local_src: Path | None = None,
|
|
411
411
|
git_ref: str | None = None,
|
|
412
|
+
skill_path: str | None = None,
|
|
412
413
|
) -> InstallResult:
|
|
413
414
|
"""Материализует навык в стор и линкует в scope агента.
|
|
414
415
|
|
|
@@ -449,6 +450,7 @@ class SkillStore:
|
|
|
449
450
|
slug=slug, dir_name=dir_name, version=version, commit_sha=commit_sha,
|
|
450
451
|
repo_url=repo_url, manifest=manifest, scope=scope, project=project,
|
|
451
452
|
skill_id=skill_id_str, local_src=local_src, git_ref=git_ref,
|
|
453
|
+
skill_path=skill_path,
|
|
452
454
|
)
|
|
453
455
|
# git-url источник мог уточнить версию из frontmatter клона (фикс l2).
|
|
454
456
|
effective_version = mat.get("version") or version
|
|
@@ -518,10 +520,12 @@ class SkillStore:
|
|
|
518
520
|
skill_id: str | int | None = None,
|
|
519
521
|
local_src: Path | None = None,
|
|
520
522
|
git_ref: str | None = None,
|
|
523
|
+
skill_path: str | None = None,
|
|
521
524
|
) -> dict[str, Any]:
|
|
522
525
|
"""Материализует контент навыка в стор БЕЗ линковки в scope.
|
|
523
526
|
|
|
524
|
-
|
|
527
|
+
``skill_path`` (#268) — подпапка навыка в репо (``skills/<name>/``);
|
|
528
|
+
None ⇒ корень репо. Возвращает {is_update, filter_result, update_diff, version}.
|
|
525
529
|
"""
|
|
526
530
|
scope = "project" if project is not None else "global"
|
|
527
531
|
dir_name = skill_dir_name(slug, skill_id)
|
|
@@ -530,12 +534,13 @@ class SkillStore:
|
|
|
530
534
|
slug=slug, dir_name=dir_name, version=version, commit_sha=commit_sha,
|
|
531
535
|
repo_url=repo_url, manifest=manifest, scope=scope, project=project,
|
|
532
536
|
skill_id=skill_id_str, local_src=local_src, git_ref=git_ref,
|
|
537
|
+
skill_path=skill_path,
|
|
533
538
|
)
|
|
534
539
|
|
|
535
540
|
def _materialize_store(
|
|
536
541
|
self, *, slug, dir_name, version, commit_sha, repo_url, manifest,
|
|
537
542
|
scope, project, skill_id, local_src: Path | None = None,
|
|
538
|
-
git_ref: str | None = None,
|
|
543
|
+
git_ref: str | None = None, skill_path: str | None = None,
|
|
539
544
|
) -> dict[str, Any]:
|
|
540
545
|
"""Кладёт контент навыка в store_dir. Возвращает {is_update, filter_result, update_diff}."""
|
|
541
546
|
store_dir = self._store_path(dir_name)
|
|
@@ -550,6 +555,7 @@ class SkillStore:
|
|
|
550
555
|
slug=slug, version=version, commit_sha=commit_sha, repo_url=repo_url,
|
|
551
556
|
manifest=manifest, scope=scope, project=project, slug_dir=store_dir,
|
|
552
557
|
skill_id=skill_id, local_src=local_src, git_ref=git_ref,
|
|
558
|
+
skill_path=skill_path,
|
|
553
559
|
)
|
|
554
560
|
return {"is_update": True, "filter_result": up.filter_result,
|
|
555
561
|
"update_diff": up.update_diff, "version": up.version}
|
|
@@ -558,7 +564,9 @@ class SkillStore:
|
|
|
558
564
|
filter_result: dict[str, int] | None = None
|
|
559
565
|
source = "hub"
|
|
560
566
|
if repo_url:
|
|
561
|
-
with self._clone_version(
|
|
567
|
+
with self._clone_version(
|
|
568
|
+
repo_url, version, ref=git_ref, skill_path=skill_path
|
|
569
|
+
) as cloned:
|
|
562
570
|
safe_copy_tree(cloned, store_dir)
|
|
563
571
|
filter_result = apply_skill_filter(store_dir)
|
|
564
572
|
if git_ref is not None:
|
|
@@ -684,6 +692,7 @@ class SkillStore:
|
|
|
684
692
|
skill_id: str | None = None,
|
|
685
693
|
local_src: Path | None = None,
|
|
686
694
|
git_ref: str | None = None,
|
|
695
|
+
skill_path: str | None = None,
|
|
687
696
|
) -> InstallResult:
|
|
688
697
|
"""Докачивает diff между установленной и новой версией.
|
|
689
698
|
|
|
@@ -714,7 +723,9 @@ class SkillStore:
|
|
|
714
723
|
src_label = "local-path"
|
|
715
724
|
meta_repo = None
|
|
716
725
|
else:
|
|
717
|
-
with self._clone_version(
|
|
726
|
+
with self._clone_version(
|
|
727
|
+
repo_url, version, ref=git_ref, skill_path=skill_path
|
|
728
|
+
) as cloned:
|
|
718
729
|
safe_copy_tree(cloned, slug_dir)
|
|
719
730
|
src_label = "git-url"
|
|
720
731
|
meta_repo = repo_url
|
|
@@ -755,7 +766,7 @@ class SkillStore:
|
|
|
755
766
|
# версии (как git-url путь), сохраняя preserved-пути, вместо diff.
|
|
756
767
|
if not manifest.get("files"):
|
|
757
768
|
preserved = _preserved_for(manifest, self._target)
|
|
758
|
-
with self._clone_version(repo_url, version) as cloned:
|
|
769
|
+
with self._clone_version(repo_url, version, skill_path=skill_path) as cloned:
|
|
759
770
|
safe_copy_tree(cloned, slug_dir)
|
|
760
771
|
version = _version_from_skill_md(slug_dir, version)
|
|
761
772
|
filter_result = apply_skill_filter(slug_dir, extra_preserved=preserved)
|
|
@@ -776,7 +787,7 @@ class SkillStore:
|
|
|
776
787
|
preserved = _preserved_for(manifest, self._target)
|
|
777
788
|
|
|
778
789
|
filter_result: dict[str, int] | None = None
|
|
779
|
-
with self._clone_version(repo_url, version) as cloned:
|
|
790
|
+
with self._clone_version(repo_url, version, skill_path=skill_path) as cloned:
|
|
780
791
|
# 4. Копируем added + changed.
|
|
781
792
|
for rel in [*added, *changed]:
|
|
782
793
|
src_file = cloned / rel
|
|
@@ -913,7 +924,10 @@ class SkillStore:
|
|
|
913
924
|
# helpers
|
|
914
925
|
# ------------------------------------------------------------------
|
|
915
926
|
@contextlib.contextmanager
|
|
916
|
-
def _clone_version(
|
|
927
|
+
def _clone_version(
|
|
928
|
+
self, repo_url: str, version: str, *, ref: str | None = None,
|
|
929
|
+
skill_path: str | None = None,
|
|
930
|
+
):
|
|
917
931
|
"""git clone во временную папку (yield Path).
|
|
918
932
|
|
|
919
933
|
Поведение по ``ref``:
|
|
@@ -921,7 +935,11 @@ class SkillStore:
|
|
|
921
935
|
- ``""`` (пусто) → git-url без явного ref: клон дефолтной ветки репо
|
|
922
936
|
(``--branch`` опускается — ``--branch HEAD`` git не принимает);
|
|
923
937
|
- иначе → дословный ref (ветка/тег/sha).
|
|
924
|
-
|
|
938
|
+
|
|
939
|
+
``skill_path`` (subdir-навык, #268): если задан — yield-им ПОДПАПКУ
|
|
940
|
+
``<clone>/<skill_path>`` (с traversal-guard), а не корень репо. Так навык
|
|
941
|
+
может лежать подкаталогом CLI-репо (``skills/<name>/``), а материализуется
|
|
942
|
+
только он. Папка удаляется при выходе из контекста.
|
|
925
943
|
"""
|
|
926
944
|
use_default_branch = ref == ""
|
|
927
945
|
ref = ref if ref is not None else f"v{version}"
|
|
@@ -949,7 +967,15 @@ class SkillStore:
|
|
|
949
967
|
if secret:
|
|
950
968
|
stderr = stderr.replace(secret, "***")
|
|
951
969
|
raise RuntimeError(f"git clone failed: {stderr}") from e
|
|
952
|
-
|
|
970
|
+
# subdir-навык (#268): материализуем подпапку, не корень репо.
|
|
971
|
+
target = clone_dir
|
|
972
|
+
if skill_path:
|
|
973
|
+
target = _assert_within(clone_dir, clone_dir / skill_path)
|
|
974
|
+
if not target.is_dir():
|
|
975
|
+
raise RuntimeError(
|
|
976
|
+
f"skill_path '{skill_path}' не найден в репо {repo_url}"
|
|
977
|
+
)
|
|
978
|
+
yield target
|
|
953
979
|
finally:
|
|
954
980
|
_force_rmtree(tmp)
|
|
955
981
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""#268 — subdir-навык: материализуется ПОДПАПКА репо (skills/<name>/), не корень.
|
|
2
|
+
|
|
3
|
+
Позволяет навыку жить подкаталогом CLI-репо: install клонирует репо, но в стор
|
|
4
|
+
кладёт только skill_path-подпапку (SKILL.md + agents + references), а сам CLI
|
|
5
|
+
ставится через runtime_dependency.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from skillkit import ClaudeCodeTarget, SkillStore
|
|
14
|
+
from skillkit import installer as installer_mod
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _sha(text: str) -> str:
|
|
18
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _manifest(files: dict[str, str]) -> dict[str, Any]:
|
|
22
|
+
return {
|
|
23
|
+
"version": "1.0.0", "description": "x",
|
|
24
|
+
"files": [
|
|
25
|
+
{"path": rel, "sha256": _sha(c), "size": len(c)} for rel, c in files.items()
|
|
26
|
+
],
|
|
27
|
+
"preserved_paths": ["_local/"],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_fake_clone(layout: dict[str, str]):
|
|
32
|
+
"""Фейковый git clone: пишет ВЕСЬ layout репо (корень + подпапки) в target."""
|
|
33
|
+
def fake_run(cmd: list[str], *a: Any, **k: Any):
|
|
34
|
+
assert cmd[0] == "git" and cmd[1] == "clone"
|
|
35
|
+
target = Path(cmd[-1])
|
|
36
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
for rel, content in layout.items():
|
|
38
|
+
p = target / rel
|
|
39
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
p.write_text(content, encoding="utf-8")
|
|
41
|
+
|
|
42
|
+
class _R:
|
|
43
|
+
returncode = 0
|
|
44
|
+
stdout = ""
|
|
45
|
+
stderr = ""
|
|
46
|
+
|
|
47
|
+
return _R()
|
|
48
|
+
|
|
49
|
+
return fake_run
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Репо CLI-проекта: код в корне + навык в skills/atlas/
|
|
53
|
+
_REPO = {
|
|
54
|
+
"pyproject.toml": "[project]\nname='atlas'\n",
|
|
55
|
+
"src/atlas/cli.py": "# CLI",
|
|
56
|
+
"skills/atlas/SKILL.md": "---\nname: atlas\nversion: 1.0.0\n---\n# atlas skill",
|
|
57
|
+
"skills/atlas/agents/x.md": "AGENT",
|
|
58
|
+
"skills/atlas/references/r.md": "REF",
|
|
59
|
+
}
|
|
60
|
+
_SKILL_FILES = {
|
|
61
|
+
"SKILL.md": _REPO["skills/atlas/SKILL.md"],
|
|
62
|
+
"agents/x.md": "AGENT",
|
|
63
|
+
"references/r.md": "REF",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_subdir_materialize_takes_only_skill_path(tmp_path: Path, monkeypatch) -> None:
|
|
68
|
+
monkeypatch.setattr(installer_mod.subprocess, "run", _make_fake_clone(_REPO))
|
|
69
|
+
store = SkillStore(ClaudeCodeTarget(root=tmp_path / ".claude"), tmp_path / "store")
|
|
70
|
+
store.materialize(
|
|
71
|
+
slug="atlas", version="1.0.0", commit_sha="a",
|
|
72
|
+
repo_url="https://github.com/x/atlas-cli.git",
|
|
73
|
+
manifest=_manifest(_SKILL_FILES),
|
|
74
|
+
skill_path="skills/atlas",
|
|
75
|
+
)
|
|
76
|
+
store_dir = tmp_path / "store" / "atlas"
|
|
77
|
+
# В сторе — содержимое ПОДПАПКИ навыка:
|
|
78
|
+
assert (store_dir / "SKILL.md").exists()
|
|
79
|
+
assert (store_dir / "agents" / "x.md").read_text(encoding="utf-8") == "AGENT"
|
|
80
|
+
assert (store_dir / "references" / "r.md").exists()
|
|
81
|
+
# НЕ корневой код репо:
|
|
82
|
+
assert not (store_dir / "pyproject.toml").exists()
|
|
83
|
+
assert not (store_dir / "src").exists()
|
|
84
|
+
assert not (store_dir / "skills").exists()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_subdir_missing_path_errors(tmp_path: Path, monkeypatch) -> None:
|
|
88
|
+
monkeypatch.setattr(installer_mod.subprocess, "run", _make_fake_clone(_REPO))
|
|
89
|
+
store = SkillStore(ClaudeCodeTarget(root=tmp_path / ".claude"), tmp_path / "store")
|
|
90
|
+
import pytest
|
|
91
|
+
with pytest.raises(RuntimeError, match="skill_path"):
|
|
92
|
+
store.materialize(
|
|
93
|
+
slug="atlas", version="1.0.0", commit_sha="a",
|
|
94
|
+
repo_url="https://github.com/x/atlas-cli.git",
|
|
95
|
+
manifest=_manifest(_SKILL_FILES),
|
|
96
|
+
skill_path="skills/nonexistent",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_no_skill_path_uses_repo_root(tmp_path: Path, monkeypatch) -> None:
|
|
101
|
+
"""Без skill_path (None) поведение прежнее — материализуется корень репо."""
|
|
102
|
+
root_layout = {"SKILL.md": "---\nname: x\nversion: 1.0.0\n---\n# x", "a.md": "A"}
|
|
103
|
+
monkeypatch.setattr(installer_mod.subprocess, "run", _make_fake_clone(root_layout))
|
|
104
|
+
store = SkillStore(ClaudeCodeTarget(root=tmp_path / ".claude"), tmp_path / "store")
|
|
105
|
+
store.materialize(
|
|
106
|
+
slug="x", version="1.0.0", commit_sha="a",
|
|
107
|
+
repo_url="https://github.com/x/x.git", manifest=_manifest(root_layout),
|
|
108
|
+
)
|
|
109
|
+
store_dir = tmp_path / "store" / "x"
|
|
110
|
+
assert (store_dir / "SKILL.md").exists()
|
|
111
|
+
assert (store_dir / "a.md").exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|