s-skillkit 0.3.2__tar.gz → 0.3.4__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.3.2 → s_skillkit-0.3.4}/CHANGELOG.md +36 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/PKG-INFO +7 -3
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/README.md +4 -2
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/pyproject.toml +18 -4
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/__init__.py +6 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/bootstrap.py +35 -20
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/deps_installer.py +28 -9
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/__init__.py +40 -6
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/fetchers.py +32 -22
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/fsops.py +26 -5
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/models.py +17 -1
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/linker.py +34 -7
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/manifest.py +10 -4
- s_skillkit-0.3.4/skillkit/telemetry.py +118 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/tooling.py +15 -6
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/uv_bootstrap.py +19 -5
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_errors.py +12 -3
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_installer.py +36 -1
- s_skillkit-0.3.4/tests/test_no_raw_subprocess.py +77 -0
- s_skillkit-0.3.4/tests/test_pkgsrc_not_in_agent_zone.py +102 -0
- s_skillkit-0.3.4/tests/test_telemetry_adapter.py +211 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_tooling.py +3 -3
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_uv_bootstrap.py +3 -2
- s_skillkit-0.3.4/uv.lock +580 -0
- s_skillkit-0.3.2/uv.lock +0 -139
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/.gitignore +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/.gitlab-ci.yml +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/AGENTS.md +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/LICENSE +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/binary_installer.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/collections.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/config.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/errors.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/filter.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/hook_register.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/differ.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/meta.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/installer/ownership.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/mcp_register.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/path_store.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/paths.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/pkg_manager.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/project.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/__init__.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/antigravity.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/base.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/claude_code.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/codex.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/detect.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/skillkit/targets/opencode.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/conftest.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_agent_skill_layouts_2026_07_21.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_binary_installer.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_bootstrap.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_collections.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_config.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_deps_binary.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_deps_installer.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_destructive_ownership_gate_2026_07_21.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_filter.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_git_install.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_hooks.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_legacy_paths_no_orphans_2026_07_21.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_manifest.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_mcp_register.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_packaging_lock.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_path_store.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_paths.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_pkg_manager.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_project.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_remove_ownership_guard_2026_07_21.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_stub_guard.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_targets.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_transactional_install.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_traversal_guard.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_typed_fields.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_w268_subdir_skill.py +0 -0
- {s_skillkit-0.3.2 → s_skillkit-0.3.4}/tests/test_w272_empty_manifest_guard.py +0 -0
|
@@ -4,6 +4,42 @@
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/ru/1.1.0/); версионирование —
|
|
5
5
|
[SemVer](https://semver.org/lang/ru/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Fixed — окна консоли, `.pkgsrc` в зоне агента, потеря происхождения снапшота
|
|
10
|
+
|
|
11
|
+
- **#1144 — прямые `subprocess` выкошены.** Демон стартует DETACHED (своей
|
|
12
|
+
консоли нет), поэтому КАЖДЫЙ консольный ребёнок (`uv`, `git`, `powershell`,
|
|
13
|
+
`cmd`) без `CREATE_NO_WINDOW` получал от Windows своё окно — пользователь
|
|
14
|
+
видел `uv.EXE` поверх браузера во время фоновой установки навыка.
|
|
15
|
+
`capture_output=True` окно НЕ подавляет (stdio и аллокация консоли в Win32 —
|
|
16
|
+
разные механизмы), и флаг стоял ровно в ОДНОМ из девяти мест. Все запуски
|
|
17
|
+
переведены на `librarykit.proc.run` (единая политика окон + обязательный
|
|
18
|
+
таймаут + захват вывода + маскировка секретов в логе). Появилась зависимость
|
|
19
|
+
`s-librarykit>=0.7.1`. Стережёт AST-гейт `tests/test_no_raw_subprocess.py`:
|
|
20
|
+
прямой `subprocess.run|Popen|call|check_output|check_call` в `skillkit/**`
|
|
21
|
+
роняет тесты.
|
|
22
|
+
- **#1147 — `.pkgsrc` больше не утекает в папку агента.** При copy-fallback
|
|
23
|
+
(линковка стор→scope не удалась) копировался ВЕСЬ стор: `safe_copy_tree`
|
|
24
|
+
фильтровал только `.git`. Фильтр стал параметром (`skip_roots`), в зону агента
|
|
25
|
+
едет `AGENT_ZONE_SKIP_ROOTS` (`.git`, `.pkgsrc`). Копия стор→staging (перенос
|
|
26
|
+
версии) фильтр НЕ применяет — `.pkgsrc` обязана переезжать между версиями,
|
|
27
|
+
иначе теряется имя дистрибутива для `uv tool uninstall`. Инвариант: **стор —
|
|
28
|
+
источник истины; в зоне агента только артефакты навыка**. Плюс диагностика
|
|
29
|
+
первопричины: причина fallback'а логируется и различает «канон занят обычной
|
|
30
|
+
папкой» (`FileExistsError`) от «ОС/ФС не дала ссылку», а на Windows перед
|
|
31
|
+
сдачей в copy теперь пробуется `os.symlink` (Developer Mode), которого раньше
|
|
32
|
+
не пробовали вовсе.
|
|
33
|
+
- **#1145 — снапшот-установка метится `source="hub"`.** Ядро нормализует
|
|
34
|
+
`SnapshotSource` → `LocalPathSource` (архив распакован во временную папку) ДО
|
|
35
|
+
записи меты, и происхождение терялось: 7 из 8 навыков с хаба получали
|
|
36
|
+
`source="local-path"` и МОЛЧА выпадали из автообновления CLI (оно берёт только
|
|
37
|
+
`source == "hub"`). Происхождение теперь едет явной меткой
|
|
38
|
+
(`SnapshotSource.origin` → `InstallRequest.source_label`) и перебивает вывод по
|
|
39
|
+
типу источника — и на установке, и на обновлении. Семантика реального
|
|
40
|
+
`install_from_path` не изменилась. Миграция уже установленных сторов — на
|
|
41
|
+
стороне CLI (признак: `source == "local-path"` при непустом `skill_id`).
|
|
42
|
+
|
|
7
43
|
## [0.3.1] — 2026-07-21
|
|
8
44
|
|
|
9
45
|
Круг 3 независимой проверки нашёл два настоящих дефекта в правках 0.3.0 и один
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s-skillkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -8,6 +8,8 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Requires-Dist: pathspec>=0.12.0
|
|
10
10
|
Requires-Dist: platformdirs>=4.0
|
|
11
|
+
Requires-Dist: s-librarykit>=0.7.1
|
|
12
|
+
Requires-Dist: s-telemetrykit>=0.2.0
|
|
11
13
|
Requires-Dist: tomli-w>=1.0
|
|
12
14
|
Provides-Extra: dev
|
|
13
15
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
@@ -16,8 +18,10 @@ Description-Content-Type: text/markdown
|
|
|
16
18
|
|
|
17
19
|
# s-skillkit
|
|
18
20
|
|
|
19
|
-
Локальное ядро управления навыками (skills) —
|
|
20
|
-
|
|
21
|
+
Локальное ядро управления навыками (skills): только локальная ФС-механика — ни
|
|
22
|
+
сети, ни auth. Из `librarykit` берётся ровно один модуль — `librarykit.proc`
|
|
23
|
+
(единый запуск подпроцессов: на Windows без всплывающих консольных окон и с
|
|
24
|
+
обязательным таймаутом); `uv`, `git` и `npm` кит зовёт только через него.
|
|
21
25
|
|
|
22
26
|
## Что внутри
|
|
23
27
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# s-skillkit
|
|
2
2
|
|
|
3
|
-
Локальное ядро управления навыками (skills) —
|
|
4
|
-
|
|
3
|
+
Локальное ядро управления навыками (skills): только локальная ФС-механика — ни
|
|
4
|
+
сети, ни auth. Из `librarykit` берётся ровно один модуль — `librarykit.proc`
|
|
5
|
+
(единый запуск подпроцессов: на Windows без всплывающих консольных окон и с
|
|
6
|
+
обязательным таймаутом); `uv`, `git` и `npm` кит зовёт только через него.
|
|
5
7
|
|
|
6
8
|
## Что внутри
|
|
7
9
|
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "s-skillkit"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
7
7
|
requires-python = ">=3.11"
|
|
8
8
|
authors = [{ name = "Dmitry" }]
|
|
9
|
-
# skillkit —
|
|
10
|
-
#
|
|
9
|
+
# skillkit — локальная ФС-механика навыков (НЕ ходит в сеть, не знает auth).
|
|
10
|
+
# Core deps:
|
|
11
11
|
# tomli-w — запись TOML (project-манифест .skills-hub/skills.toml,
|
|
12
12
|
# _skill_meta.toml ридеры используют stdlib tomllib на чтение);
|
|
13
13
|
# pathspec — gitignore-стиль .skillignore / files-allowlist фильтр;
|
|
14
|
-
# platformdirs — нативные дефолтные пути (Paths.default) по
|
|
14
|
+
# platformdirs — нативные дефолтные пути (Paths.default) по ОС;
|
|
15
|
+
# s-librarykit — ТОЛЬКО ради ``librarykit.proc`` (#1144): единая политика запуска
|
|
16
|
+
# подпроцессов. Кит запускает uv/git/npm/powershell, а на Windows консольный
|
|
17
|
+
# ребёнок демона (у него нет своей консоли) без CREATE_NO_WINDOW получает
|
|
18
|
+
# ВИДИМОЕ окно поверх браузера; ``capture_output`` его не подавляет. Своей
|
|
19
|
+
# второй реализации «как правильно на Windows» в ките быть не должно —
|
|
20
|
+
# поэтому прежний статус «sibling, ни от чего не зависим» осознанно снят.
|
|
21
|
+
# s-telemetrykit — продуктово-нейтральный SDK учёта запусков. Кит нейтрален
|
|
22
|
+
# намеренно (он переиспользуем в любом продукте), поэтому skillery-специфику
|
|
23
|
+
# телеметрии — каталог ~/.skillery, префикс SKILLERY_*, канон _skill_meta.* и
|
|
24
|
+
# kind="skill_run" — держит адаптер `skillkit.telemetry`: канон меты навыка это
|
|
25
|
+
# ответственность ИМЕННО этого кита. Зависимость лёгкая: telemetrykit
|
|
26
|
+
# stdlib-only и своих зависимостей не имеет.
|
|
15
27
|
dependencies = [
|
|
16
28
|
"tomli-w>=1.0",
|
|
17
29
|
"pathspec>=0.12.0",
|
|
18
30
|
"platformdirs>=4.0",
|
|
31
|
+
"s-librarykit>=0.7.1",
|
|
32
|
+
"s-telemetrykit>=0.2.0",
|
|
19
33
|
]
|
|
20
34
|
|
|
21
35
|
[project.optional-dependencies]
|
|
@@ -27,6 +27,12 @@ platformdirs. Отвечает за ФС-механику навыка:
|
|
|
27
27
|
бинари с GitHub Releases), ``bootstrap.bootstrap_skill`` (ensure_uv → deps →
|
|
28
28
|
бинари → gate → self_check) + ``bootstrap.run_self_check``.
|
|
29
29
|
|
|
30
|
+
- ``telemetry`` — skillery-адаптер продуктово-нейтрального ``telemetrykit``:
|
|
31
|
+
``~/.skillery`` + префикс ``SKILLERY_*`` + канон ``_skill_meta.*``/``SKILL.md``
|
|
32
|
+
+ ``kind="skill_run"``. Импортируется ОТДЕЛЬНО (``from skillkit.telemetry
|
|
33
|
+
import track_skill``) и НЕ входит в eager-импорты пакета: применение
|
|
34
|
+
конфигурации — побочный эффект, он не должен случаться от ``import skillkit``.
|
|
35
|
+
|
|
30
36
|
Хаб-специфика (track_skill_event, install-by-slug bundle, /me/installs) живёт в
|
|
31
37
|
CLI, НЕ здесь.
|
|
32
38
|
"""
|
|
@@ -22,23 +22,36 @@ s-xray-gate). Кит НЕ зависит от него жёстко: это ``Ca
|
|
|
22
22
|
запускает ``scripts/self_check.py`` (venv-python навыка → ``uv run`` → текущий
|
|
23
23
|
интерпретатор), возвращает ``(ok, output)``.
|
|
24
24
|
|
|
25
|
-
Мокабельность:
|
|
25
|
+
Мокабельность: запуск идёт через ``_run_cmd`` (uv sync) и ``_run_capture``
|
|
26
26
|
(self_check); поиск uv — через ``_which``; ОС — константа ``IS_WINDOWS``.
|
|
27
|
+
Оба примитива — обёртки над ``librarykit.proc.run`` (#1144): на Windows он
|
|
28
|
+
всегда ставит ``CREATE_NO_WINDOW`` (bootstrap зовёт демон, у которого нет своей
|
|
29
|
+
консоли — иначе окна поверх браузера) и требует явный таймаут.
|
|
27
30
|
"""
|
|
28
31
|
from __future__ import annotations
|
|
29
32
|
|
|
33
|
+
import logging
|
|
30
34
|
import shutil
|
|
31
|
-
import subprocess
|
|
32
35
|
import sys
|
|
33
36
|
from collections.abc import Callable
|
|
34
37
|
from pathlib import Path
|
|
35
38
|
from typing import Any
|
|
36
39
|
|
|
40
|
+
from librarykit import proc
|
|
41
|
+
from librarykit.proc import ProcResult
|
|
42
|
+
|
|
37
43
|
from skillkit import deps_installer, uv_bootstrap
|
|
38
44
|
from skillkit.deps_installer import _BINARY_KINDS
|
|
39
45
|
|
|
40
46
|
IS_WINDOWS = sys.platform == "win32"
|
|
41
47
|
|
|
48
|
+
_LOG = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
#: ``uv sync`` тянет всё дерево зависимостей навыка — это минуты на узком канале.
|
|
51
|
+
_SYNC_TIMEOUT = 900.0
|
|
52
|
+
#: self_check навыка — проверка окружения, а не сборка; висеть ей незачем.
|
|
53
|
+
_SELF_CHECK_TIMEOUT = 300.0
|
|
54
|
+
|
|
42
55
|
|
|
43
56
|
# --------------------------------------------------------------------------
|
|
44
57
|
# Подменяемые в тестах примитивы окружения
|
|
@@ -50,27 +63,28 @@ def _which(name: str) -> str | None:
|
|
|
50
63
|
def _run_cmd(cmd: list[str], *, cwd: Path | None = None) -> int:
|
|
51
64
|
"""Запустить команду, вернуть returncode (не бросает на OSError)."""
|
|
52
65
|
try:
|
|
53
|
-
return
|
|
54
|
-
cmd, cwd=str(cwd) if cwd else None, check=False
|
|
55
|
-
).returncode
|
|
66
|
+
return proc.run(cmd, cwd=cwd, timeout=_SYNC_TIMEOUT, log=_LOG).returncode
|
|
56
67
|
except OSError:
|
|
57
68
|
return 127
|
|
58
69
|
|
|
59
70
|
|
|
60
|
-
def _run_capture(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
def _run_capture(cmd: list[str], *, cwd: Path | None = None) -> ProcResult:
|
|
72
|
+
"""Запустить команду с захватом вывода (не бросает на OSError).
|
|
73
|
+
|
|
74
|
+
Возвращает ``ProcResult`` — по полям (``returncode``/``stdout``/``stderr``)
|
|
75
|
+
совместим с прежним ``CompletedProcess``, но дополнительно несёт ``timed_out``.
|
|
76
|
+
"""
|
|
64
77
|
try:
|
|
65
|
-
return
|
|
66
|
-
cmd,
|
|
67
|
-
cwd=str(cwd) if cwd else None,
|
|
68
|
-
check=False,
|
|
69
|
-
capture_output=True,
|
|
70
|
-
text=True,
|
|
71
|
-
)
|
|
78
|
+
return proc.run(cmd, cwd=cwd, timeout=_SELF_CHECK_TIMEOUT, log=_LOG)
|
|
72
79
|
except OSError as exc:
|
|
73
|
-
return
|
|
80
|
+
return ProcResult(
|
|
81
|
+
cmd=tuple(str(c) for c in cmd),
|
|
82
|
+
returncode=127,
|
|
83
|
+
stdout="",
|
|
84
|
+
stderr=str(exc),
|
|
85
|
+
duration=0.0,
|
|
86
|
+
timed_out=False,
|
|
87
|
+
)
|
|
74
88
|
|
|
75
89
|
|
|
76
90
|
# --------------------------------------------------------------------------
|
|
@@ -131,9 +145,10 @@ def run_self_check(skill_dir: Path | str) -> tuple[bool, str]:
|
|
|
131
145
|
script = skill_dir / "scripts" / "self_check.py"
|
|
132
146
|
if not script.exists():
|
|
133
147
|
return (False, f"scripts/self_check.py не найден в {skill_dir}")
|
|
134
|
-
proc
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
# Имя ``result``, а не ``proc``: модульный ``proc`` — это librarykit.proc.
|
|
149
|
+
result = _run_capture(_self_check_command(skill_dir, script), cwd=skill_dir)
|
|
150
|
+
output = (result.stdout or "") + (result.stderr or "")
|
|
151
|
+
return (result.returncode == 0, output)
|
|
137
152
|
|
|
138
153
|
|
|
139
154
|
# Алиас для использования внутри bootstrap_skill (там параметр ``run_self_check``
|
|
@@ -27,19 +27,34 @@
|
|
|
27
27
|
|
|
28
28
|
Кроссплатформенность: выбор менеджера — через ``shutil.which`` (обёрнут в
|
|
29
29
|
``_which`` для мокабельности) + ``python -m pip`` (текущий интерпретатор,
|
|
30
|
-
работает на win/mac/linux).
|
|
30
|
+
работает на win/mac/linux). Примитив запуска ``_run`` тоже подменяем в тестах.
|
|
31
|
+
|
|
32
|
+
Запуск идёт ТОЛЬКО через ``librarykit.proc.run`` (#1144): на Windows он всегда
|
|
33
|
+
ставит ``CREATE_NO_WINDOW`` (иначе фоновая установка навыка демоном рисует
|
|
34
|
+
пользователю окна ``uv.EXE`` поверх браузера) и требует явный таймаут — висящий
|
|
35
|
+
``uv`` в демоне некому прервать. Заодно снят прежний дефект: ``uv pip install``
|
|
36
|
+
запускался без захвата вывода и сыпал прогресс-бары в stdout потребителя.
|
|
31
37
|
"""
|
|
32
38
|
from __future__ import annotations
|
|
33
39
|
|
|
40
|
+
import logging
|
|
34
41
|
import shutil
|
|
35
|
-
import subprocess
|
|
36
42
|
import sys
|
|
37
43
|
from pathlib import Path
|
|
38
44
|
from typing import Any
|
|
39
45
|
|
|
46
|
+
from librarykit import proc
|
|
47
|
+
|
|
40
48
|
from skillkit import binary_installer
|
|
41
49
|
from skillkit.pkg_manager import detect_project, missing_manager_report
|
|
42
50
|
|
|
51
|
+
_LOG = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
#: Установка пакета — минуты (компиляция колёс, узкий канал), а не секунды.
|
|
54
|
+
_INSTALL_TIMEOUT = 900.0
|
|
55
|
+
#: Проба «есть ли pip» обязана отвечать быстро; вис здесь = вис установки.
|
|
56
|
+
_PROBE_TIMEOUT = 60.0
|
|
57
|
+
|
|
43
58
|
# kinds внешних бинарей (GitHub Releases) — делегируют binary_installer.
|
|
44
59
|
_BINARY_KINDS = frozenset({"binary", "github-release"})
|
|
45
60
|
|
|
@@ -58,20 +73,24 @@ def _which(name: str) -> str | None:
|
|
|
58
73
|
def _pip_available() -> bool:
|
|
59
74
|
"""Доступен ли ``pip`` для текущего интерпретатора (``python -m pip``)."""
|
|
60
75
|
try:
|
|
61
|
-
proc
|
|
76
|
+
return proc.run(
|
|
62
77
|
[sys.executable, "-m", "pip", "--version"],
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
return proc.returncode == 0
|
|
78
|
+
timeout=_PROBE_TIMEOUT,
|
|
79
|
+
log=_LOG,
|
|
80
|
+
).ok
|
|
67
81
|
except OSError:
|
|
68
82
|
return False
|
|
69
83
|
|
|
70
84
|
|
|
71
85
|
def _run(cmd: list[str]) -> int:
|
|
72
|
-
"""Запустить команду установки, вернуть returncode (не бросает на OSError).
|
|
86
|
+
"""Запустить команду установки, вернуть returncode (не бросает на OSError).
|
|
87
|
+
|
|
88
|
+
Таймаут — не ошибка вызывающего, а такой же неуспех: ``ProcResult.ok`` уже
|
|
89
|
+
False, а ``returncode`` при убийстве по таймауту равен -1 (отличимо от кода
|
|
90
|
+
менеджера в отчёте ``failed``).
|
|
91
|
+
"""
|
|
73
92
|
try:
|
|
74
|
-
return
|
|
93
|
+
return proc.run(cmd, timeout=_INSTALL_TIMEOUT, log=_LOG).returncode
|
|
75
94
|
except OSError:
|
|
76
95
|
return 127
|
|
77
96
|
|
|
@@ -27,7 +27,12 @@ skillkit-инъекция: store_dir передаётся явно (Paths.store_
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
29
|
import contextlib
|
|
30
|
-
import
|
|
30
|
+
import logging
|
|
31
|
+
|
|
32
|
+
# re-export seam: тесты патчат installer.subprocess.run. Сам кит подпроцессы
|
|
33
|
+
# НЕ запускает напрямую (#1144) — их гоняет ``librarykit.proc.run``, который
|
|
34
|
+
# внутри зовёт этот же stdlib-``subprocess``, поэтому патч продолжает работать.
|
|
35
|
+
import subprocess # noqa: F401
|
|
31
36
|
import tarfile
|
|
32
37
|
import tempfile
|
|
33
38
|
from dataclasses import replace
|
|
@@ -52,6 +57,7 @@ from skillkit.installer.differ import _file_index, _manifest_diff # noqa: F401
|
|
|
52
57
|
from skillkit.installer.fetchers import ContentFetcher, GitContentFetcher
|
|
53
58
|
from skillkit.installer.fsops import (
|
|
54
59
|
_COPY_SKIP_ROOT, # noqa: F401 # re-export seam
|
|
60
|
+
AGENT_ZONE_SKIP_ROOTS,
|
|
55
61
|
PathTraversalError,
|
|
56
62
|
_assert_within,
|
|
57
63
|
_force_rmtree, # noqa: F401 # re-export seam (звать ЗДЕСЬ нельзя — только PathGuard)
|
|
@@ -132,6 +138,9 @@ __all__ = [
|
|
|
132
138
|
]
|
|
133
139
|
|
|
134
140
|
|
|
141
|
+
_LOG = logging.getLogger(__name__)
|
|
142
|
+
|
|
143
|
+
|
|
135
144
|
# Брендо-нейтральные дефолты (само-имя кита); потребитель переопределяет через
|
|
136
145
|
# ``set_managed_by`` / ``set_clone_prefix`` (обычно из ``config.configure`` —
|
|
137
146
|
# выводятся из ``app_name``).
|
|
@@ -282,9 +291,13 @@ class SkillStore:
|
|
|
282
291
|
# папку и ставим её как локальный источник (как прежний
|
|
283
292
|
# ``install_from_snapshot`` → ``install_from_path``).
|
|
284
293
|
if isinstance(request.source, SnapshotSource):
|
|
294
|
+
# #1145: происхождение архива НЕ теряется при нормализации —
|
|
295
|
+
# оно едет дальше явной меткой и перебивает вывод по типу источника
|
|
296
|
+
# (иначе мета получит "local-path" и навык выпадет из автоапдейта).
|
|
297
|
+
origin = request.source_label or request.source.origin
|
|
285
298
|
with self._extract_snapshot(request.source.archive_path) as root:
|
|
286
299
|
return self._install(
|
|
287
|
-
replace(request, source=LocalPathSource(root))
|
|
300
|
+
replace(request, source=LocalPathSource(root), source_label=origin)
|
|
288
301
|
)
|
|
289
302
|
|
|
290
303
|
slug = request.slug
|
|
@@ -531,7 +544,9 @@ class SkillStore:
|
|
|
531
544
|
self._build_meta(
|
|
532
545
|
request.slug, version, request.commit_sha, request.manifest, scope,
|
|
533
546
|
request.project, self._skill_id_str(request),
|
|
534
|
-
|
|
547
|
+
# Явная метка (снапшот хаба) сильнее вывода по типу источника.
|
|
548
|
+
source=request.source_label or source_label,
|
|
549
|
+
repo_url=repo_url_meta,
|
|
535
550
|
skill_path=_source_skill_path(src),
|
|
536
551
|
),
|
|
537
552
|
)
|
|
@@ -560,9 +575,24 @@ class SkillStore:
|
|
|
560
575
|
try:
|
|
561
576
|
kind = linker.create_link(link, store_dir)
|
|
562
577
|
return True, kind
|
|
563
|
-
except OSError:
|
|
578
|
+
except OSError as exc:
|
|
564
579
|
# Нет прав / ФС не поддерживает ссылки → копируем стор в scope.
|
|
565
|
-
|
|
580
|
+
# #1147: причину fallback'а раньше никто не видел — а лечатся эти
|
|
581
|
+
# случаи по-разному. FileExistsError = канон занят ОБЫЧНОЙ папкой
|
|
582
|
+
# (её должен был освободить гейт, значит проблема выше по стеку);
|
|
583
|
+
# прочий OSError = среда (нет прав на symlink, ФС без ссылок).
|
|
584
|
+
reason = (
|
|
585
|
+
"канон занят обычной папкой"
|
|
586
|
+
if isinstance(exc, FileExistsError)
|
|
587
|
+
else "ОС/ФС не дала создать ссылку"
|
|
588
|
+
)
|
|
589
|
+
_LOG.warning(
|
|
590
|
+
"линковка %s → %s не удалась (%s: %s); копирую стор в scope",
|
|
591
|
+
link, store_dir, reason, exc,
|
|
592
|
+
)
|
|
593
|
+
# В зону агента едут ТОЛЬКО артефакты навыка: служебные каталоги
|
|
594
|
+
# (``.pkgsrc``) остаются в сторе — источнике истины.
|
|
595
|
+
safe_copy_tree(store_dir, link, skip_roots=AGENT_ZONE_SKIP_ROOTS)
|
|
566
596
|
return False, "copy"
|
|
567
597
|
|
|
568
598
|
def link_existing(
|
|
@@ -747,7 +777,11 @@ class SkillStore:
|
|
|
747
777
|
slug_dir,
|
|
748
778
|
self._build_meta(
|
|
749
779
|
request.slug, version, request.commit_sha, request.manifest, scope,
|
|
750
|
-
request.project, skill_id,
|
|
780
|
+
request.project, skill_id,
|
|
781
|
+
# #1145: и на ветке ОБНОВЛЕНИЯ метка снапшота обязана уцелеть —
|
|
782
|
+
# иначе навык выпадал из автоапдейта после первого же update.
|
|
783
|
+
source=request.source_label or src_label,
|
|
784
|
+
repo_url=meta_repo,
|
|
751
785
|
skill_path=_source_skill_path(request.source),
|
|
752
786
|
),
|
|
753
787
|
)
|
|
@@ -2,28 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
``ContentFetcher`` — протокол «достань репозиторий навыка на диск как контекст-
|
|
4
4
|
менеджер, отдай Path на корень клона, по выходу — прибери за собой». Дефолт —
|
|
5
|
-
``GitContentFetcher`` (прежнее поведение: ``git clone --depth 1``
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
``
|
|
12
|
-
|
|
5
|
+
``GitContentFetcher`` (прежнее поведение: ``git clone --depth 1``). Инъекция
|
|
6
|
+
позволяет подменить получение источника (офлайн-зеркало, кеш, фейк в тестах)
|
|
7
|
+
НЕ трогая оркестратор ``SkillStore``.
|
|
8
|
+
|
|
9
|
+
Запуск git идёт через ``librarykit.proc.run`` (#1144) — единая политика окон и
|
|
10
|
+
таймаутов кита. Здесь исторически стоял ЕДИНСТВЕННЫЙ во всём ките
|
|
11
|
+
``CREATE_NO_WINDOW``; теперь флаг не дублируется в вызове, а приходит из ``proc``.
|
|
12
|
+
|
|
13
|
+
Важно про seam: ``proc.run`` внутри зовёт ``subprocess.run``, поэтому
|
|
14
|
+
существующий в тестах ``monkeypatch.setattr(installer.subprocess, "run", ...)``
|
|
15
|
+
(патч атрибута общего stdlib-модуля) продолжает перехватывать клон.
|
|
13
16
|
"""
|
|
14
17
|
from __future__ import annotations
|
|
15
18
|
|
|
16
19
|
import contextlib
|
|
17
|
-
import
|
|
18
|
-
import sys
|
|
20
|
+
import logging
|
|
19
21
|
import tempfile
|
|
20
22
|
from collections.abc import Iterator
|
|
21
23
|
from pathlib import Path
|
|
22
24
|
from typing import Protocol, runtime_checkable
|
|
23
25
|
|
|
26
|
+
from librarykit import proc
|
|
27
|
+
|
|
24
28
|
from skillkit.errors import CloneError
|
|
25
29
|
from skillkit.installer.fsops import _force_rmtree
|
|
26
30
|
|
|
31
|
+
_LOG = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
#: ``git clone --depth 1`` навыка: обычно секунды, но узкий канал/большой репо —
|
|
34
|
+
#: минуты. Без таймаута зависший git держал бы фоновую установку вечно.
|
|
35
|
+
_CLONE_TIMEOUT = 900.0
|
|
36
|
+
|
|
27
37
|
|
|
28
38
|
@runtime_checkable
|
|
29
39
|
class ContentFetcher(Protocol):
|
|
@@ -78,24 +88,24 @@ class GitContentFetcher:
|
|
|
78
88
|
if not use_default_branch:
|
|
79
89
|
cmd += ["--branch", ref]
|
|
80
90
|
cmd += [url, str(clone_dir)]
|
|
81
|
-
# #1028: без окна на Windows. Когда git clone спавнит ДЕМОН (без
|
|
82
|
-
# консоли), Windows отдаёт git собственное окно git-bash → мигающая
|
|
83
|
-
# вкладка. CREATE_NO_WINDOW подавляет её. Вне Windows флага нет.
|
|
84
|
-
_run_kw: dict = {"check": True, "capture_output": True, "text": True}
|
|
85
|
-
if sys.platform == "win32":
|
|
86
|
-
_run_kw["creationflags"] = 0x08000000 # CREATE_NO_WINDOW
|
|
87
91
|
try:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
# #1028/#1144: без окна на Windows. Когда git clone спавнит ДЕМОН
|
|
93
|
+
# (без консоли), Windows отдаёт git собственное окно — мигающая
|
|
94
|
+
# вкладка поверх браузера. CREATE_NO_WINDOW ставит сам ``proc.run``.
|
|
95
|
+
# ``log=`` маскирует креды в URL, но stderr самого git может нести
|
|
96
|
+
# токен дословно — эту редакцию делаем ниже сами.
|
|
97
|
+
result = proc.run(cmd, timeout=_CLONE_TIMEOUT, log=_LOG)
|
|
98
|
+
if not result.ok:
|
|
99
|
+
stderr = result.stderr or result.stdout or ""
|
|
100
|
+
if result.timed_out:
|
|
101
|
+
stderr = f"таймаут {_CLONE_TIMEOUT:.0f}s. {stderr}"
|
|
92
102
|
# Маскируем секрет из URL в сообщении об ошибке: если резолвер
|
|
93
|
-
# добавил токен в URL, не утечь его в текст
|
|
103
|
+
# добавил токен в URL, не утечь его в текст CloneError.
|
|
94
104
|
if url != repo_url and "@" in url:
|
|
95
105
|
secret = url.split("//", 1)[-1].split("@", 1)[0]
|
|
96
106
|
if secret:
|
|
97
107
|
stderr = stderr.replace(secret, "***")
|
|
98
|
-
raise CloneError(f"git clone failed: {stderr}")
|
|
108
|
+
raise CloneError(f"git clone failed: {stderr}")
|
|
99
109
|
yield clone_dir
|
|
100
110
|
finally:
|
|
101
111
|
_force_rmtree(tmp)
|
|
@@ -22,6 +22,16 @@ class PathTraversalError(SkillkitError):
|
|
|
22
22
|
# Внутри source-репо НЕ копируем в slug_dir (state репозитория, не skill).
|
|
23
23
|
_COPY_SKIP_ROOT = frozenset({".git"})
|
|
24
24
|
|
|
25
|
+
#: Что НЕ имеет права попасть в зону агента (``~/.claude/skills/<slug>/``) при
|
|
26
|
+
#: copy-fallback (#1147). Инвариант: **стор — источник истины; в зоне агента
|
|
27
|
+
#: только артефакты навыка**. ``.pkgsrc`` — служебный исходник пакета навыка,
|
|
28
|
+
#: он нужен ``uv tool uninstall`` при снятии и живёт в СТОРЕ; скопированный
|
|
29
|
+
#: рядом со ``SKILL.md`` он выглядел «лишней папкой, дублирующей снапшот».
|
|
30
|
+
#: ВАЖНО: этот набор применяется ТОЛЬКО к копии в зону агента. Копия
|
|
31
|
+
#: стор→staging (перенос версии) обязана нести ``.pkgsrc`` дальше, иначе имя
|
|
32
|
+
#: дистрибутива для uninstall теряется навсегда.
|
|
33
|
+
AGENT_ZONE_SKIP_ROOTS = frozenset({".git", ".pkgsrc"})
|
|
34
|
+
|
|
25
35
|
|
|
26
36
|
def _on_rm_error(func, path, exc_info):
|
|
27
37
|
"""Чинит read-only-файлы в Windows (.git/objects/...) перед удалением."""
|
|
@@ -111,9 +121,16 @@ def _assert_within(base: Path, candidate: Path) -> Path:
|
|
|
111
121
|
return Path(cand_abs)
|
|
112
122
|
|
|
113
123
|
|
|
114
|
-
def safe_copy_tree(
|
|
124
|
+
def safe_copy_tree(
|
|
125
|
+
src: Path, dst: Path, *, skip_roots: frozenset[str] | set[str] | None = None
|
|
126
|
+
) -> None:
|
|
115
127
|
"""Копирует содержимое src → dst, отвергая path-traversal (ТЗ §10).
|
|
116
128
|
|
|
129
|
+
``skip_roots`` — какие каталоги ВЕРХНЕГО уровня не копировать; по умолчанию
|
|
130
|
+
``_COPY_SKIP_ROOT`` (только ``.git``). Копирование в зону агента передаёт
|
|
131
|
+
сюда ``AGENT_ZONE_SKIP_ROOTS`` (плюс ``.pkgsrc``, см. #1147) — фильтр стал
|
|
132
|
+
параметром именно потому, что он РАЗНЫЙ для стора и для зоны агента.
|
|
133
|
+
|
|
117
134
|
Правила безопасности:
|
|
118
135
|
- `.git/` source-репо пропускается (это state репо, не содержимое skill).
|
|
119
136
|
- Ссылки НЕ следуются вслепую; если цель ссылки резолвится наружу dst →
|
|
@@ -129,6 +146,7 @@ def safe_copy_tree(src: Path, dst: Path) -> None:
|
|
|
129
146
|
"""
|
|
130
147
|
from skillkit import linker
|
|
131
148
|
|
|
149
|
+
skips = frozenset(skip_roots) if skip_roots is not None else _COPY_SKIP_ROOT
|
|
132
150
|
src = Path(src)
|
|
133
151
|
dst = Path(dst)
|
|
134
152
|
dst.mkdir(parents=True, exist_ok=True)
|
|
@@ -140,11 +158,12 @@ def safe_copy_tree(src: Path, dst: Path) -> None:
|
|
|
140
158
|
for root, dirnames, filenames in os.walk(src, followlinks=False):
|
|
141
159
|
root_path = Path(root)
|
|
142
160
|
rel_root = root_path.relative_to(src)
|
|
143
|
-
# Пропускаем .git
|
|
144
|
-
|
|
161
|
+
# Пропускаем корневые skip-каталоги (.git, при копии в зону агента —
|
|
162
|
+
# ещё и .pkgsrc) и не спускаемся внутрь.
|
|
163
|
+
if rel_root.parts and rel_root.parts[0] in skips:
|
|
145
164
|
dirnames[:] = []
|
|
146
165
|
continue
|
|
147
|
-
dirnames[:] = [d for d in dirnames if d not in
|
|
166
|
+
dirnames[:] = [d for d in dirnames if d not in skips]
|
|
148
167
|
|
|
149
168
|
linked_dirs: list[str] = []
|
|
150
169
|
for name in list(dirnames):
|
|
@@ -160,7 +179,9 @@ def safe_copy_tree(src: Path, dst: Path) -> None:
|
|
|
160
179
|
# он НЕ отсекает (followlinks=False ловит только symlink) и
|
|
161
180
|
# спустился бы внутрь, дублируя/обходя guard.
|
|
162
181
|
target.mkdir(parents=True, exist_ok=True)
|
|
163
|
-
|
|
182
|
+
# Тот же набор исключений и внутри материализуемой ссылки —
|
|
183
|
+
# иначе фильтр «в зону агента» обходился бы через junction.
|
|
184
|
+
safe_copy_tree(child, target, skip_roots=skips)
|
|
164
185
|
linked_dirs.append(name)
|
|
165
186
|
else:
|
|
166
187
|
target.mkdir(parents=True, exist_ok=True)
|
|
@@ -89,9 +89,20 @@ class LocalPathSource:
|
|
|
89
89
|
|
|
90
90
|
@dataclass(frozen=True)
|
|
91
91
|
class SnapshotSource:
|
|
92
|
-
"""Snapshot-архив навыка (``tar.gz``): распаковывается и ставится как local.
|
|
92
|
+
"""Snapshot-архив навыка (``tar.gz``): распаковывается и ставится как local.
|
|
93
|
+
|
|
94
|
+
``origin`` (#1145) — ЧЕМ этот архив является по происхождению; попадает в
|
|
95
|
+
``_skill_meta.json`` как ``source``. Дефолт ``"hub"``: снапшот — это раздача
|
|
96
|
+
хаба (``GET /skills/{slug}/versions/{semver}/snapshot``), а не локальная
|
|
97
|
+
папка пользователя. Поле нужно потому, что ядро нормализует ``SnapshotSource``
|
|
98
|
+
→ ``LocalPathSource`` (архив распакован во временную папку) ДО записи меты, и
|
|
99
|
+
без него происхождение терялось: мета получала ``"local-path"``, а авто-
|
|
100
|
+
обновление CLI берёт только ``source == "hub"`` — установленные снапшотом
|
|
101
|
+
навыки молча выпадали из автоапдейта.
|
|
102
|
+
"""
|
|
93
103
|
|
|
94
104
|
archive_path: Path
|
|
105
|
+
origin: Source = "hub"
|
|
95
106
|
|
|
96
107
|
|
|
97
108
|
@dataclass(frozen=True)
|
|
@@ -140,6 +151,11 @@ class InstallRequest:
|
|
|
140
151
|
project: Path | None = None
|
|
141
152
|
force: bool = False
|
|
142
153
|
skill_id: str | int | None = None
|
|
154
|
+
#: Явная метка происхождения для ``_skill_meta.json`` (#1145). None ⇒ метка
|
|
155
|
+
#: выводится из типа ``source`` (как раньше). Задаётся, когда вывод по типу
|
|
156
|
+
#: соврал бы: снапшот хаба ставится ЧЕРЕЗ ``LocalPathSource`` (распакованная
|
|
157
|
+
#: временная папка), но по происхождению это "hub".
|
|
158
|
+
source_label: Source | None = None
|
|
143
159
|
|
|
144
160
|
|
|
145
161
|
@dataclass
|