mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
Merge branch 'dev'
This commit is contained in:
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -0,0 +1,4 @@
|
||||
scripts/node_modules/
|
||||
.browser-session.json
|
||||
*.png
|
||||
*.mp4
|
||||
+12
-2
@@ -16,6 +16,9 @@ test-tmp/
|
||||
# Инструменты (portable Apache и т.д.)
|
||||
tools/
|
||||
|
||||
# Отладка навыков (eval, trigger-test, run_loop результаты)
|
||||
debug/
|
||||
|
||||
# Python кэш
|
||||
__pycache__/
|
||||
|
||||
@@ -31,8 +34,15 @@ __pycache__/
|
||||
*.mp4
|
||||
|
||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
||||
.agents/
|
||||
.augment/
|
||||
.cline/
|
||||
.codex/
|
||||
.cursor/skills/
|
||||
.github/skills/
|
||||
.cursor/
|
||||
.gemini/
|
||||
.github/skills/
|
||||
.kilocode/
|
||||
.kiro/
|
||||
.opencode/
|
||||
.roo/
|
||||
.windsurf/
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
```
|
||||
МойПроект/
|
||||
├── .claude/skills/ ← скопировать из этого репозитория
|
||||
├── src/ ← исходники (создаются навыками)
|
||||
└── ...
|
||||
```
|
||||
|
||||
@@ -52,12 +51,15 @@
|
||||
Навыки построены на открытом стандарте [Agent Skills](https://agentskills.io/specification) и совместимы с любой платформой, поддерживающей этот формат. Скрипт `switch.py` копирует навыки в нужный каталог с перезаписью путей:
|
||||
|
||||
```bash
|
||||
python scripts/switch.py # интерактивный режим (пошаговый диалог)
|
||||
python scripts/switch.py cursor # скопировать навыки для Cursor
|
||||
python scripts/switch.py cursor --runtime python # Cursor + Python-рантайм
|
||||
python scripts/switch.py --undo cursor # удалить копию
|
||||
python scripts/switch.py # интерактивный режим
|
||||
python scripts/switch.py cursor # скопировать навыки для Cursor
|
||||
python scripts/switch.py cursor --runtime python # Cursor + Python-рантайм
|
||||
python scripts/switch.py claude-code --project-dir /my/proj # установить в другой проект
|
||||
python scripts/switch.py --undo cursor # удалить копию
|
||||
```
|
||||
|
||||
Если репозиторий склонирован внутрь проекта (например, в `tools/cc-1c-skills`), используйте `--project-dir` для установки навыков в целевой проект. Обновление — `git pull` и повторный запуск.
|
||||
|
||||
Поддерживаемые платформы:
|
||||
|
||||
| Платформа | Целевой каталог | `switch.py <platform>` |
|
||||
|
||||
+221
-71
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# switch.py v1.1 — Переключение навыков 1С между AI-платформами и рантаймами
|
||||
# switch.py v1.2 — Переключение навыков 1С между AI-платформами и рантаймами
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""
|
||||
Копирует навыки из .claude/skills/ на другие AI-платформы (Cursor, Codex, Copilot,
|
||||
@@ -7,11 +7,12 @@ Kiro, Gemini CLI, OpenCode, Windsurf, Kilo Code, Cline, Roo Code, Augment и д
|
||||
с перезаписью путей, и/или переключает рантайм (PowerShell ↔ Python).
|
||||
|
||||
Использование:
|
||||
python scripts/switch.py # интерактивный режим
|
||||
python scripts/switch.py cursor # скопировать на Cursor
|
||||
python scripts/switch.py cursor --runtime python # скопировать + Python
|
||||
python scripts/switch.py --undo cursor # удалить копию
|
||||
python scripts/switch.py --runtime python # сменить runtime in-place
|
||||
python scripts/switch.py # интерактивный режим
|
||||
python scripts/switch.py cursor # скопировать на Cursor
|
||||
python scripts/switch.py cursor --runtime python # скопировать + Python
|
||||
python scripts/switch.py claude-code --project-dir /my/proj # установить в проект
|
||||
python scripts/switch.py --undo cursor # удалить копию
|
||||
python scripts/switch.py --runtime python # сменить runtime in-place
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
@@ -41,6 +42,16 @@ PLATFORMS = {
|
||||
|
||||
SOURCE_PREFIX = '.claude/skills'
|
||||
|
||||
# Рекомендуемые записи для .gitignore целевого проекта
|
||||
GITIGNORE_RECOMMENDATIONS = [
|
||||
'.v8-project.json',
|
||||
'build/',
|
||||
'base/',
|
||||
'*.epf',
|
||||
'*.erf',
|
||||
'*.log',
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime regex patterns (from switch-to-python.py / switch-to-powershell.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -72,6 +83,52 @@ def collect_md_files(skill_dir):
|
||||
return sorted(glob.glob(os.path.join(skill_dir, '*.md')))
|
||||
|
||||
|
||||
def classify_skill_runtime(skill_dir):
|
||||
"""Classify skill runtime based on invocations in .md files.
|
||||
|
||||
Returns 'ps', 'py', 'both', or 'none'.
|
||||
"""
|
||||
has_ps = has_py = False
|
||||
for md_path in collect_md_files(skill_dir):
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if RX_PS.search(content):
|
||||
has_ps = True
|
||||
if RX_PY.search(content):
|
||||
has_py = True
|
||||
if has_ps and has_py:
|
||||
return 'both'
|
||||
return 'ps' if has_ps else ('py' if has_py else 'none')
|
||||
|
||||
|
||||
def check_missing_files(skill_dir, target_runtime, root):
|
||||
"""Check if target runtime script files exist for a skill.
|
||||
|
||||
Returns list of missing file paths (relative to root).
|
||||
"""
|
||||
missing = []
|
||||
for md_path in collect_md_files(skill_dir):
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if target_runtime == 'python':
|
||||
for m in RX_PS.findall(content):
|
||||
py_path = m.lstrip("'") + '.py'
|
||||
if not os.path.isfile(os.path.join(root, py_path)):
|
||||
missing.append(py_path)
|
||||
elif target_runtime == 'powershell':
|
||||
for m in RX_PY.findall(content):
|
||||
ps1_path = m.lstrip("'") + '.ps1'
|
||||
if not os.path.isfile(os.path.join(root, ps1_path)):
|
||||
missing.append(ps1_path)
|
||||
return missing
|
||||
|
||||
|
||||
def is_different_dir(dir1, dir2):
|
||||
"""Check if two directories are different (resolved)."""
|
||||
return os.path.normcase(os.path.realpath(dir1)) != \
|
||||
os.path.normcase(os.path.realpath(dir2))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transformations
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -91,27 +148,59 @@ def switch_runtime_content(content, target_runtime):
|
||||
return new, new != content
|
||||
|
||||
|
||||
def check_runtime_files(skills_dir, target_runtime, root):
|
||||
"""Check that target runtime script files exist. Returns list of warnings."""
|
||||
warnings = []
|
||||
for skill_name in scan_skills(skills_dir):
|
||||
for md_path in collect_md_files(os.path.join(skills_dir, skill_name)):
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
def print_gitignore_recommendations(project_dir):
|
||||
"""Print .gitignore recommendations for the target project."""
|
||||
gitignore_path = os.path.join(project_dir, '.gitignore')
|
||||
existing = set()
|
||||
if os.path.isfile(gitignore_path):
|
||||
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
existing.add(line.strip())
|
||||
|
||||
if target_runtime == 'python':
|
||||
matches = RX_PS.findall(content)
|
||||
for m in matches:
|
||||
py_path = m.lstrip("'") + '.py'
|
||||
if not os.path.isfile(os.path.join(root, py_path)):
|
||||
warnings.append(f" {py_path} не найден")
|
||||
elif target_runtime == 'powershell':
|
||||
matches = RX_PY.findall(content)
|
||||
for m in matches:
|
||||
ps1_path = m.lstrip("'") + '.ps1'
|
||||
if not os.path.isfile(os.path.join(root, ps1_path)):
|
||||
warnings.append(f" {ps1_path} не найден")
|
||||
return warnings
|
||||
missing = [r for r in GITIGNORE_RECOMMENDATIONS if r not in existing]
|
||||
if missing:
|
||||
print(f"\nРекомендуется добавить в .gitignore проекта:")
|
||||
for r in missing:
|
||||
print(f" {r}")
|
||||
|
||||
|
||||
def collect_runtime_messages(skill_name, skill_dir, target_runtime, root):
|
||||
"""Check runtime compatibility for a skill.
|
||||
|
||||
Returns (info_list, warning_list).
|
||||
"""
|
||||
info = []
|
||||
warnings = []
|
||||
src_rt = classify_skill_runtime(skill_dir)
|
||||
|
||||
if target_runtime == 'python' and src_rt in ('ps', 'none'):
|
||||
missing = check_missing_files(skill_dir, 'python', root)
|
||||
if missing:
|
||||
info.append(f" {skill_name} — только PowerShell "
|
||||
f"(Python-версия не предусмотрена)")
|
||||
elif target_runtime == 'powershell' and src_rt in ('py', 'none'):
|
||||
missing = check_missing_files(skill_dir, 'powershell', root)
|
||||
if missing:
|
||||
info.append(f" {skill_name} — только Python "
|
||||
f"(PowerShell-версия не предусмотрена)")
|
||||
else:
|
||||
missing = check_missing_files(skill_dir, target_runtime, root)
|
||||
for m in missing:
|
||||
warnings.append(f" {m} не найден ({skill_name})")
|
||||
|
||||
return info, warnings
|
||||
|
||||
|
||||
def print_runtime_messages(info, warnings):
|
||||
"""Print collected info and warning messages."""
|
||||
if info:
|
||||
print(f"\nИнформация:")
|
||||
for i in info:
|
||||
print(i)
|
||||
if warnings:
|
||||
print(f"\nПредупреждения (отсутствующие файлы):")
|
||||
for w in warnings:
|
||||
print(w)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -136,8 +225,15 @@ def cmd_install(platform, runtime, project_dir):
|
||||
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
# Copy root-level files from source skills dir (.gitignore, etc.)
|
||||
for name in os.listdir(src_dir):
|
||||
src_path = os.path.join(src_dir, name)
|
||||
if os.path.isfile(src_path):
|
||||
shutil.copy2(src_path, os.path.join(target_dir, name))
|
||||
|
||||
installed = 0
|
||||
warnings = []
|
||||
all_info = []
|
||||
all_warnings = []
|
||||
|
||||
print(f"\nКопирование {len(skills)} навыков в {target_prefix}/ ...")
|
||||
|
||||
@@ -145,6 +241,15 @@ def cmd_install(platform, runtime, project_dir):
|
||||
src_skill = os.path.join(src_dir, skill_name)
|
||||
dst_skill = os.path.join(target_dir, skill_name)
|
||||
|
||||
# Skip runtime conversion for single-runtime skills where
|
||||
# target files don't exist (e.g. img-grid has only .py)
|
||||
src_rt = classify_skill_runtime(src_skill)
|
||||
missing = check_missing_files(src_skill, runtime, repo_root())
|
||||
skip_runtime = bool(missing) and (
|
||||
(runtime == 'python' and src_rt in ('ps', 'none'))
|
||||
or (runtime == 'powershell' and src_rt in ('py', 'none'))
|
||||
)
|
||||
|
||||
# Copy entire skill directory
|
||||
shutil.copytree(src_skill, dst_skill)
|
||||
|
||||
@@ -155,30 +260,34 @@ def cmd_install(platform, runtime, project_dir):
|
||||
|
||||
new_content = rewrite_paths(content, SOURCE_PREFIX, target_prefix)
|
||||
|
||||
# Apply runtime switch if requested
|
||||
if runtime == 'python':
|
||||
new_content, _ = switch_runtime_content(new_content, 'python')
|
||||
|
||||
# Check .py files exist in source repo
|
||||
for m in RX_PS.findall(content):
|
||||
clean = m.lstrip("'").replace(SOURCE_PREFIX, target_prefix)
|
||||
original_py = m.lstrip("'") + '.py'
|
||||
if not os.path.isfile(os.path.join(repo_root(), original_py)):
|
||||
warnings.append(f" {original_py} не найден ({skill_name})")
|
||||
# Apply runtime switch (skip for single-runtime skills
|
||||
# where target runtime is not available)
|
||||
if not skip_runtime:
|
||||
if runtime == 'python':
|
||||
new_content, _ = switch_runtime_content(new_content, 'python')
|
||||
elif runtime == 'powershell':
|
||||
new_content, _ = switch_runtime_content(new_content, 'powershell')
|
||||
|
||||
if new_content != content:
|
||||
with open(md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
# Check runtime compatibility (against source)
|
||||
info, warnings = collect_runtime_messages(
|
||||
skill_name, src_skill, runtime, repo_root())
|
||||
all_info.extend(info)
|
||||
all_warnings.extend(warnings)
|
||||
|
||||
print(f" [OK] {skill_name}")
|
||||
installed += 1
|
||||
|
||||
print(f"\nГотово! {installed} навыков установлено в {target_prefix}/")
|
||||
if warnings:
|
||||
print("\nПредупреждения (отсутствующие .py файлы):")
|
||||
for w in warnings:
|
||||
print(w)
|
||||
print(f"\nДля удаления: python scripts/switch.py --undo {platform}")
|
||||
|
||||
print_runtime_messages(all_info, all_warnings)
|
||||
print_gitignore_recommendations(project_dir)
|
||||
|
||||
if platform != 'claude-code':
|
||||
print(f"\nДля удаления: python scripts/switch.py --undo {platform}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -221,34 +330,37 @@ def cmd_switch_runtime(runtime, project_dir):
|
||||
|
||||
skills = scan_skills(skills_dir)
|
||||
switched = 0
|
||||
warnings = []
|
||||
all_info = []
|
||||
all_warnings = []
|
||||
|
||||
print(f"\nПереключение на {runtime} в {PLATFORMS[platform_name]}/ ...")
|
||||
|
||||
for skill_name in skills:
|
||||
skill_path = os.path.join(skills_dir, skill_name)
|
||||
|
||||
# Skip runtime conversion for single-runtime skills where
|
||||
# target files don't exist (e.g. img-grid has only .py)
|
||||
cur_rt = classify_skill_runtime(skill_path)
|
||||
missing = check_missing_files(skill_path, runtime, repo_root())
|
||||
skip_runtime = bool(missing) and (
|
||||
(runtime == 'python' and cur_rt in ('ps', 'none'))
|
||||
or (runtime == 'powershell' and cur_rt in ('py', 'none'))
|
||||
)
|
||||
|
||||
info, warnings = collect_runtime_messages(
|
||||
skill_name, skill_path, runtime, repo_root())
|
||||
all_info.extend(info)
|
||||
all_warnings.extend(warnings)
|
||||
|
||||
if skip_runtime:
|
||||
continue
|
||||
|
||||
for md_path in collect_md_files(skill_path):
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
new_content, changed = switch_runtime_content(content, runtime)
|
||||
|
||||
# Check target files exist
|
||||
if runtime == 'python':
|
||||
for m in RX_PS.findall(content):
|
||||
py_path = m.lstrip("'") + '.py'
|
||||
full = os.path.join(repo_root(), py_path)
|
||||
if not os.path.isfile(full):
|
||||
md_name = os.path.basename(md_path)
|
||||
warnings.append(f" {py_path} не найден ({skill_name}/{md_name})")
|
||||
elif runtime == 'powershell':
|
||||
for m in RX_PY.findall(content):
|
||||
ps1_path = m.lstrip("'") + '.ps1'
|
||||
full = os.path.join(repo_root(), ps1_path)
|
||||
if not os.path.isfile(full):
|
||||
md_name = os.path.basename(md_path)
|
||||
warnings.append(f" {ps1_path} не найден ({skill_name}/{md_name})")
|
||||
|
||||
if changed:
|
||||
with open(md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
@@ -257,10 +369,7 @@ def cmd_switch_runtime(runtime, project_dir):
|
||||
switched += 1
|
||||
|
||||
print(f"\nПереключено {switched} файлов на {runtime}.")
|
||||
if warnings:
|
||||
print(f"\nПредупреждения (отсутствующие файлы):")
|
||||
for w in warnings:
|
||||
print(w)
|
||||
print_runtime_messages(all_info, all_warnings)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -289,6 +398,17 @@ def ask_choice(prompt, options, default=1):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def ask_path(prompt, default=''):
|
||||
"""Ask user for a directory path."""
|
||||
hint = f" [{default}]" if default else ""
|
||||
try:
|
||||
raw = input(f"\n{prompt}{hint}: ").strip()
|
||||
return raw if raw else default
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nОтмена.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def interactive_mode():
|
||||
"""Run interactive setup wizard."""
|
||||
print("Навыки 1С — настройка платформы")
|
||||
@@ -317,12 +437,32 @@ def interactive_mode():
|
||||
choice = ask_choice("Для какой платформы настроить навыки?", platform_options)
|
||||
platform = platform_keys[choice - 1]
|
||||
|
||||
# Check if already installed — offer update or remove
|
||||
project_dir = os.getcwd()
|
||||
install_mode = True
|
||||
|
||||
# For claude-code in repo root, offer runtime switch as alternative
|
||||
if platform == 'claude-code' and not is_different_dir(project_dir, repo_root()):
|
||||
mode_options = [
|
||||
("Переключить runtime", "сменить PowerShell \u2194 Python в текущем проекте"),
|
||||
("Установить в проект", "скопировать навыки в другой проект"),
|
||||
]
|
||||
mode = ask_choice("Что сделать?", mode_options)
|
||||
install_mode = (mode == 2)
|
||||
|
||||
# Ask for project directory when installing
|
||||
if install_mode:
|
||||
default_dir = project_dir
|
||||
project_dir = ask_path("Путь к целевому проекту", default_dir)
|
||||
if not project_dir or not os.path.isdir(project_dir):
|
||||
print(f"Ошибка: директория '{project_dir}' не найдена.",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Check if already installed — offer update or remove
|
||||
target_prefix = PLATFORMS[platform]
|
||||
target_dir = os.path.join(project_dir, target_prefix.replace('/', os.sep))
|
||||
|
||||
if platform != 'claude-code' and os.path.isdir(target_dir):
|
||||
if install_mode and os.path.isdir(target_dir):
|
||||
existing = scan_skills(target_dir)
|
||||
if existing:
|
||||
action_options = [
|
||||
@@ -347,10 +487,10 @@ def interactive_mode():
|
||||
rt_choice = ask_choice("Какой рантайм скриптов?", runtime_options)
|
||||
runtime = 'powershell' if rt_choice == 1 else 'python'
|
||||
|
||||
if platform == 'claude-code':
|
||||
return cmd_switch_runtime(runtime, project_dir)
|
||||
else:
|
||||
if install_mode:
|
||||
return cmd_install(platform, runtime, project_dir)
|
||||
else:
|
||||
return cmd_switch_runtime(runtime, project_dir)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -365,6 +505,7 @@ def main():
|
||||
epilog='Примеры:\n'
|
||||
' python scripts/switch.py cursor\n'
|
||||
' python scripts/switch.py cursor --runtime python\n'
|
||||
' python scripts/switch.py claude-code --project-dir /my/proj\n'
|
||||
' python scripts/switch.py --undo cursor\n'
|
||||
' python scripts/switch.py --runtime python\n',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
@@ -384,8 +525,10 @@ def main():
|
||||
if args.undo:
|
||||
if not args.platform:
|
||||
parser.error("--undo требует указания платформы")
|
||||
if args.platform == 'claude-code':
|
||||
parser.error("--undo не применим к claude-code (это исходная платформа)")
|
||||
if args.platform == 'claude-code' \
|
||||
and not is_different_dir(args.project_dir, repo_root()):
|
||||
parser.error(
|
||||
"--undo не применим к claude-code в исходном репозитории")
|
||||
return cmd_undo(args.platform, args.project_dir)
|
||||
|
||||
# --runtime without platform = in-place switch
|
||||
@@ -395,10 +538,17 @@ def main():
|
||||
# platform specified
|
||||
if args.platform:
|
||||
if args.platform == 'claude-code':
|
||||
# claude-code + different project-dir → install
|
||||
if is_different_dir(args.project_dir, repo_root()):
|
||||
runtime = args.runtime or 'powershell'
|
||||
return cmd_install(args.platform, runtime, args.project_dir)
|
||||
# claude-code in repo root → runtime switch only
|
||||
if args.runtime:
|
||||
return cmd_switch_runtime(args.runtime, args.project_dir)
|
||||
else:
|
||||
parser.error("для claude-code укажите --runtime python или --runtime powershell")
|
||||
parser.error(
|
||||
"для claude-code без --project-dir укажите "
|
||||
"--runtime python или --runtime powershell")
|
||||
runtime = args.runtime or 'powershell'
|
||||
return cmd_install(args.platform, runtime, args.project_dir)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user