s-skillkit 0.1.0__tar.gz → 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.
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/PKG-INFO +1 -1
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/pyproject.toml +48 -48
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/deps_installer.py +189 -185
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/installer.py +25 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_deps_installer.py +202 -167
- s_skillkit-0.1.2/tests/test_w272_empty_manifest_guard.py +114 -0
- s_skillkit-0.1.2/uv.lock +139 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/.gitignore +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/LICENSE +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/README.md +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/__init__.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/collections.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/filter.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/linker.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/manifest.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/mcp_register.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/path_store.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/paths.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/project.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/targets/__init__.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/targets/antigravity.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/targets/base.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/targets/claude_code.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/targets/codex.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/targets/detect.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/skillkit/tooling.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/conftest.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_collections.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_filter.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_git_install.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_installer.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_manifest.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_mcp_register.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_path_store.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_paths.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_project.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_stub_guard.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_targets.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_tooling.py +0 -0
- {s_skillkit-0.1.0 → s_skillkit-0.1.2}/tests/test_traversal_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.2
|
|
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.2"
|
|
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,185 +1,189 @@
|
|
|
1
|
-
"""Установка runtime-зависимостей навыка (tooling).
|
|
2
|
-
|
|
3
|
-
Навык типа ``tooling`` может декларировать ``runtime_dependencies`` —
|
|
4
|
-
список ``{kind, spec}``, где ``kind ∈ {pip, npm, system}``:
|
|
5
|
-
|
|
6
|
-
- ``pip`` → ставим через **uv** (``uv pip install``) → fallback ``pip``
|
|
7
|
-
(``python -m pip install``). Нет ни uv, ни pip → НЕ падаем, кладём в
|
|
8
|
-
``skipped`` с готовой инструкцией (graceful degradation);
|
|
9
|
-
- ``npm`` → ставим через ``npm install -g`` только если ``npm`` есть в PATH;
|
|
10
|
-
иначе ``skipped`` + инструкция (node/npm — не наша забота ставить);
|
|
11
|
-
- ``system`` → НИКОГДА не ставим сами (apt/brew/choco/права) — только
|
|
12
|
-
инструкция в ``skipped``.
|
|
13
|
-
|
|
14
|
-
Идемпотентность — на менеджере пакетов (повторный install уже стоящего = no-op).
|
|
15
|
-
Функция НИКОГДА не бросает: ошибка любой зависимости → она в ``failed``/
|
|
16
|
-
``skipped``, остальные продолжают. Возврат::
|
|
17
|
-
|
|
18
|
-
{"installed": [{kind, spec}, ...],
|
|
19
|
-
"skipped": [{kind, spec, reason, instruction}, ...],
|
|
20
|
-
"failed": [{kind, spec, reason}, ...]}
|
|
21
|
-
|
|
22
|
-
Кроссплатформенность: выбор менеджера — через ``shutil.which`` (обёрнут в
|
|
23
|
-
``_which`` для мокабельности) + ``python -m pip`` (текущий интерпретатор,
|
|
24
|
-
работает на win/mac/linux). subprocess-примитив ``_run`` тоже подменяем в тестах.
|
|
25
|
-
"""
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import shutil
|
|
29
|
-
import subprocess
|
|
30
|
-
import sys
|
|
31
|
-
from typing import Any
|
|
32
|
-
|
|
33
|
-
# kinds, которые умеем обрабатывать (прочее → skipped с reason unknown-kind).
|
|
34
|
-
_KNOWN_KINDS = frozenset({"pip", "npm", "system"})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# --------------------------------------------------------------------------
|
|
38
|
-
# Подменяемые в тестах примитивы окружения
|
|
39
|
-
# --------------------------------------------------------------------------
|
|
40
|
-
def _which(name: str) -> str | None:
|
|
41
|
-
"""Путь к исполняемому ``name`` в PATH или None (обёртка для моков)."""
|
|
42
|
-
return shutil.which(name)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _pip_available() -> bool:
|
|
46
|
-
"""Доступен ли ``pip`` для текущего интерпретатора (``python -m pip``)."""
|
|
47
|
-
try:
|
|
48
|
-
proc = subprocess.run(
|
|
49
|
-
[sys.executable, "-m", "pip", "--version"],
|
|
50
|
-
capture_output=True,
|
|
51
|
-
check=False,
|
|
52
|
-
)
|
|
53
|
-
return proc.returncode == 0
|
|
54
|
-
except OSError:
|
|
55
|
-
return False
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _run(cmd: list[str]) -> int:
|
|
59
|
-
"""Запустить команду установки, вернуть returncode (не бросает на OSError)."""
|
|
60
|
-
try:
|
|
61
|
-
return subprocess.run(cmd, check=False).returncode
|
|
62
|
-
except OSError:
|
|
63
|
-
return 127
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# --------------------------------------------------------------------------
|
|
67
|
-
# Public API
|
|
68
|
-
# --------------------------------------------------------------------------
|
|
69
|
-
def install_runtime_dependencies(
|
|
70
|
-
deps: list[dict[str, Any]] | tuple[dict[str, Any], ...] | None,
|
|
71
|
-
) -> dict[str, list[dict[str, Any]]]:
|
|
72
|
-
"""Поставить runtime-зависимости навыка. Никогда не бросает.
|
|
73
|
-
|
|
74
|
-
``deps`` — список ``{"kind": pip|npm|system, "spec": "..."}`` (как в манифесте
|
|
75
|
-
``runtime_dependencies`` / ``aggregated_runtime_dependencies``). Кривые
|
|
76
|
-
элементы (не-dict, без kind/spec, неизвестный kind) пропускаются/идут в
|
|
77
|
-
skipped. Возвращает отчёт ``{installed, skipped, failed}``.
|
|
78
|
-
"""
|
|
79
|
-
report: dict[str, list[dict[str, Any]]] = {
|
|
80
|
-
"installed": [],
|
|
81
|
-
"skipped": [],
|
|
82
|
-
"failed": [],
|
|
83
|
-
}
|
|
84
|
-
if not deps:
|
|
85
|
-
return report
|
|
86
|
-
|
|
87
|
-
for raw in deps:
|
|
88
|
-
if not isinstance(raw, dict):
|
|
89
|
-
continue # мусор — игнор
|
|
90
|
-
kind = raw.get("kind")
|
|
91
|
-
spec = raw.get("spec")
|
|
92
|
-
if not kind or not spec:
|
|
93
|
-
continue # неполная декларация — игнор
|
|
94
|
-
kind = str(kind)
|
|
95
|
-
spec = str(spec)
|
|
96
|
-
if kind not in _KNOWN_KINDS:
|
|
97
|
-
report["skipped"].append(
|
|
98
|
-
{
|
|
99
|
-
"kind": kind,
|
|
100
|
-
"spec": spec,
|
|
101
|
-
"reason": f"неизвестный kind зависимости: {kind}",
|
|
102
|
-
"instruction": f"установите {spec} вручную ({kind}).",
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
continue
|
|
106
|
-
_dispatch(kind, spec, report)
|
|
107
|
-
return report
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _dispatch(
|
|
111
|
-
kind: str, spec: str, report: dict[str, list[dict[str, Any]]]
|
|
112
|
-
) -> None:
|
|
113
|
-
"""Маршрутизация одной зависимости по kind в нужную ветку установки."""
|
|
114
|
-
if kind == "pip":
|
|
115
|
-
_install_pip(spec, report)
|
|
116
|
-
elif kind == "npm":
|
|
117
|
-
_install_npm(spec, report)
|
|
118
|
-
else: # system
|
|
119
|
-
report["skipped"].append(
|
|
120
|
-
{
|
|
121
|
-
"kind": "system",
|
|
122
|
-
"spec": spec,
|
|
123
|
-
"reason": "системная зависимость не ставится автоматически",
|
|
124
|
-
"instruction": (
|
|
125
|
-
f"установите системный пакет «{spec}» вашим пакетным "
|
|
126
|
-
"менеджером (apt/brew/choco/...)."
|
|
127
|
-
),
|
|
128
|
-
}
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# --------------------------------------------------------------------------
|
|
133
|
-
# pip: uv pip install → python -m pip install
|
|
134
|
-
# --------------------------------------------------------------------------
|
|
135
|
-
def _install_pip(spec: str, report: dict[str, list[dict[str, Any]]]) -> None:
|
|
136
|
-
entry = {"kind": "pip", "spec": spec}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
1
|
+
"""Установка runtime-зависимостей навыка (tooling).
|
|
2
|
+
|
|
3
|
+
Навык типа ``tooling`` может декларировать ``runtime_dependencies`` —
|
|
4
|
+
список ``{kind, spec}``, где ``kind ∈ {pip, npm, system}``:
|
|
5
|
+
|
|
6
|
+
- ``pip`` → ставим через **uv** (``uv pip install``) → fallback ``pip``
|
|
7
|
+
(``python -m pip install``). Нет ни uv, ни pip → НЕ падаем, кладём в
|
|
8
|
+
``skipped`` с готовой инструкцией (graceful degradation);
|
|
9
|
+
- ``npm`` → ставим через ``npm install -g`` только если ``npm`` есть в PATH;
|
|
10
|
+
иначе ``skipped`` + инструкция (node/npm — не наша забота ставить);
|
|
11
|
+
- ``system`` → НИКОГДА не ставим сами (apt/brew/choco/права) — только
|
|
12
|
+
инструкция в ``skipped``.
|
|
13
|
+
|
|
14
|
+
Идемпотентность — на менеджере пакетов (повторный install уже стоящего = no-op).
|
|
15
|
+
Функция НИКОГДА не бросает: ошибка любой зависимости → она в ``failed``/
|
|
16
|
+
``skipped``, остальные продолжают. Возврат::
|
|
17
|
+
|
|
18
|
+
{"installed": [{kind, spec}, ...],
|
|
19
|
+
"skipped": [{kind, spec, reason, instruction}, ...],
|
|
20
|
+
"failed": [{kind, spec, reason}, ...]}
|
|
21
|
+
|
|
22
|
+
Кроссплатформенность: выбор менеджера — через ``shutil.which`` (обёрнут в
|
|
23
|
+
``_which`` для мокабельности) + ``python -m pip`` (текущий интерпретатор,
|
|
24
|
+
работает на win/mac/linux). subprocess-примитив ``_run`` тоже подменяем в тестах.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
# kinds, которые умеем обрабатывать (прочее → skipped с reason unknown-kind).
|
|
34
|
+
_KNOWN_KINDS = frozenset({"pip", "npm", "system"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --------------------------------------------------------------------------
|
|
38
|
+
# Подменяемые в тестах примитивы окружения
|
|
39
|
+
# --------------------------------------------------------------------------
|
|
40
|
+
def _which(name: str) -> str | None:
|
|
41
|
+
"""Путь к исполняемому ``name`` в PATH или None (обёртка для моков)."""
|
|
42
|
+
return shutil.which(name)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _pip_available() -> bool:
|
|
46
|
+
"""Доступен ли ``pip`` для текущего интерпретатора (``python -m pip``)."""
|
|
47
|
+
try:
|
|
48
|
+
proc = subprocess.run(
|
|
49
|
+
[sys.executable, "-m", "pip", "--version"],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
check=False,
|
|
52
|
+
)
|
|
53
|
+
return proc.returncode == 0
|
|
54
|
+
except OSError:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run(cmd: list[str]) -> int:
|
|
59
|
+
"""Запустить команду установки, вернуть returncode (не бросает на OSError)."""
|
|
60
|
+
try:
|
|
61
|
+
return subprocess.run(cmd, check=False).returncode
|
|
62
|
+
except OSError:
|
|
63
|
+
return 127
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --------------------------------------------------------------------------
|
|
67
|
+
# Public API
|
|
68
|
+
# --------------------------------------------------------------------------
|
|
69
|
+
def install_runtime_dependencies(
|
|
70
|
+
deps: list[dict[str, Any]] | tuple[dict[str, Any], ...] | None,
|
|
71
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
72
|
+
"""Поставить runtime-зависимости навыка. Никогда не бросает.
|
|
73
|
+
|
|
74
|
+
``deps`` — список ``{"kind": pip|npm|system, "spec": "..."}`` (как в манифесте
|
|
75
|
+
``runtime_dependencies`` / ``aggregated_runtime_dependencies``). Кривые
|
|
76
|
+
элементы (не-dict, без kind/spec, неизвестный kind) пропускаются/идут в
|
|
77
|
+
skipped. Возвращает отчёт ``{installed, skipped, failed}``.
|
|
78
|
+
"""
|
|
79
|
+
report: dict[str, list[dict[str, Any]]] = {
|
|
80
|
+
"installed": [],
|
|
81
|
+
"skipped": [],
|
|
82
|
+
"failed": [],
|
|
83
|
+
}
|
|
84
|
+
if not deps:
|
|
85
|
+
return report
|
|
86
|
+
|
|
87
|
+
for raw in deps:
|
|
88
|
+
if not isinstance(raw, dict):
|
|
89
|
+
continue # мусор — игнор
|
|
90
|
+
kind = raw.get("kind")
|
|
91
|
+
spec = raw.get("spec")
|
|
92
|
+
if not kind or not spec:
|
|
93
|
+
continue # неполная декларация — игнор
|
|
94
|
+
kind = str(kind)
|
|
95
|
+
spec = str(spec)
|
|
96
|
+
if kind not in _KNOWN_KINDS:
|
|
97
|
+
report["skipped"].append(
|
|
98
|
+
{
|
|
99
|
+
"kind": kind,
|
|
100
|
+
"spec": spec,
|
|
101
|
+
"reason": f"неизвестный kind зависимости: {kind}",
|
|
102
|
+
"instruction": f"установите {spec} вручную ({kind}).",
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
continue
|
|
106
|
+
_dispatch(kind, spec, report)
|
|
107
|
+
return report
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _dispatch(
|
|
111
|
+
kind: str, spec: str, report: dict[str, list[dict[str, Any]]]
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Маршрутизация одной зависимости по kind в нужную ветку установки."""
|
|
114
|
+
if kind == "pip":
|
|
115
|
+
_install_pip(spec, report)
|
|
116
|
+
elif kind == "npm":
|
|
117
|
+
_install_npm(spec, report)
|
|
118
|
+
else: # system
|
|
119
|
+
report["skipped"].append(
|
|
120
|
+
{
|
|
121
|
+
"kind": "system",
|
|
122
|
+
"spec": spec,
|
|
123
|
+
"reason": "системная зависимость не ставится автоматически",
|
|
124
|
+
"instruction": (
|
|
125
|
+
f"установите системный пакет «{spec}» вашим пакетным "
|
|
126
|
+
"менеджером (apt/brew/choco/...)."
|
|
127
|
+
),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------------------------------
|
|
133
|
+
# pip: uv pip install → python -m pip install
|
|
134
|
+
# --------------------------------------------------------------------------
|
|
135
|
+
def _install_pip(spec: str, report: dict[str, list[dict[str, Any]]]) -> None:
|
|
136
|
+
entry = {"kind": "pip", "spec": spec}
|
|
137
|
+
# ``--upgrade``: при обновлении навыка повторный apply «pkg>=X» должен
|
|
138
|
+
# ПОДНЯТЬ уже стоящий пакет до новой версии (без флага pip/uv считают
|
|
139
|
+
# требование удовлетворённым и ничего не делают). Идемпотентность сохраняется
|
|
140
|
+
# — для уже-последней версии --upgrade это no-op.
|
|
141
|
+
if _which("uv") is not None:
|
|
142
|
+
cmd = ["uv", "pip", "install", "--upgrade", spec]
|
|
143
|
+
elif _pip_available():
|
|
144
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", spec]
|
|
145
|
+
else:
|
|
146
|
+
report["skipped"].append(
|
|
147
|
+
{
|
|
148
|
+
**entry,
|
|
149
|
+
"reason": "не найдены ни uv, ни pip",
|
|
150
|
+
"instruction": (
|
|
151
|
+
f"установите uv (https://docs.astral.sh/uv/) или pip, затем "
|
|
152
|
+
f"`pip install {spec}`."
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
rc = _run(cmd)
|
|
158
|
+
if rc == 0:
|
|
159
|
+
report["installed"].append(entry)
|
|
160
|
+
else:
|
|
161
|
+
report["failed"].append(
|
|
162
|
+
{**entry, "reason": f"менеджер вернул код {rc}"}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
# npm: npm install -g (только если npm есть)
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
def _install_npm(spec: str, report: dict[str, list[dict[str, Any]]]) -> None:
|
|
170
|
+
entry = {"kind": "npm", "spec": spec}
|
|
171
|
+
if _which("npm") is None:
|
|
172
|
+
report["skipped"].append(
|
|
173
|
+
{
|
|
174
|
+
**entry,
|
|
175
|
+
"reason": "npm не найден в PATH",
|
|
176
|
+
"instruction": (
|
|
177
|
+
f"установите Node.js/npm (https://nodejs.org), затем "
|
|
178
|
+
f"`npm install -g {spec}`."
|
|
179
|
+
),
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
rc = _run(["npm", "install", "-g", spec])
|
|
184
|
+
if rc == 0:
|
|
185
|
+
report["installed"].append(entry)
|
|
186
|
+
else:
|
|
187
|
+
report["failed"].append(
|
|
188
|
+
{**entry, "reason": f"npm вернул код {rc}"}
|
|
189
|
+
)
|
|
@@ -747,6 +747,31 @@ class SkillStore:
|
|
|
747
747
|
is_update=True, scope=scope, skill_id=skill_id,
|
|
748
748
|
)
|
|
749
749
|
|
|
750
|
+
# BUG-3 (defence-in-depth): версия с ПУСТЫМ files-инвентарём (например,
|
|
751
|
+
# git-synced бэкендом до фикса BUG-1) НЕ должна трактоваться как «все
|
|
752
|
+
# файлы удалены». sha-diff против пустого new.files дал бы removed=ВСЕ и
|
|
753
|
+
# затёр стор (воспроизведено: update 1.0.0→1.0.1 оставил только
|
|
754
|
+
# _skill_meta.json). При наличии repo_url делаем полный ре-clone новой
|
|
755
|
+
# версии (как git-url путь), сохраняя preserved-пути, вместо diff.
|
|
756
|
+
if not manifest.get("files"):
|
|
757
|
+
preserved = _preserved_for(manifest, self._target)
|
|
758
|
+
with self._clone_version(repo_url, version) as cloned:
|
|
759
|
+
safe_copy_tree(cloned, slug_dir)
|
|
760
|
+
version = _version_from_skill_md(slug_dir, version)
|
|
761
|
+
filter_result = apply_skill_filter(slug_dir, extra_preserved=preserved)
|
|
762
|
+
write_meta(
|
|
763
|
+
slug_dir,
|
|
764
|
+
self._build_meta(
|
|
765
|
+
slug, version, commit_sha, manifest, scope, project, skill_id,
|
|
766
|
+
source="hub", repo_url=repo_url,
|
|
767
|
+
),
|
|
768
|
+
)
|
|
769
|
+
return InstallResult(
|
|
770
|
+
slug=slug, version=version, target_dir=slug_dir,
|
|
771
|
+
is_update=True, scope=scope, skill_id=skill_id,
|
|
772
|
+
filter_result=filter_result,
|
|
773
|
+
)
|
|
774
|
+
|
|
750
775
|
added, changed, removed = _manifest_diff(old_manifest, manifest)
|
|
751
776
|
preserved = _preserved_for(manifest, self._target)
|
|
752
777
|
|