Files
cc-1c-skills/scripts/switch.py
T
Nick Shirokov 7b69228d23 feat: add unified switch.py for cross-platform skill porting
Single script to copy skills between AI platforms (Cursor, Codex,
Copilot, Gemini CLI, OpenCode) with path rewriting and optional
runtime switching. Includes interactive mode for newcomers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:05:12 +03:00

393 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# switch.py v1.0 — Переключение навыков 1С между AI-платформами и рантаймами
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Копирует навыки из .claude/skills/ на другие AI-платформы (Cursor, Codex, Copilot,
Gemini CLI, OpenCode) с перезаписью путей, и/или переключает рантайм (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
"""
import argparse
import glob
import os
import re
import shutil
import sys
# ---------------------------------------------------------------------------
# Platform registry
# ---------------------------------------------------------------------------
PLATFORMS = {
'claude-code': '.claude/skills',
'codex': '.codex/skills',
'cursor': '.cursor/skills',
'copilot': '.github/skills',
'gemini': '.gemini/skills',
'opencode': '.opencode/skills',
}
SOURCE_PREFIX = '.claude/skills'
# ---------------------------------------------------------------------------
# Runtime regex patterns (from switch-to-python.py / switch-to-powershell.py)
# ---------------------------------------------------------------------------
RX_PS = re.compile(r'powershell\.exe\s+(?:-NoProfile\s+)?-File\s+(.+?)\.ps1')
RX_PY = re.compile(r"python\s+('?[\w./_-]+?)\.py")
def repo_root():
"""Return the repository root (parent of scripts/)."""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def source_skills_dir():
return os.path.join(repo_root(), '.claude', 'skills')
def scan_skills(skills_dir):
"""Return sorted list of skill directory names that contain SKILL.md."""
result = []
for entry in sorted(os.listdir(skills_dir)):
skill_path = os.path.join(skills_dir, entry)
if os.path.isdir(skill_path) and os.path.isfile(os.path.join(skill_path, 'SKILL.md')):
result.append(entry)
return result
def collect_md_files(skill_dir):
"""Return list of .md files in a skill directory."""
return sorted(glob.glob(os.path.join(skill_dir, '*.md')))
# ---------------------------------------------------------------------------
# Transformations
# ---------------------------------------------------------------------------
def rewrite_paths(content, source_prefix, target_prefix):
"""Replace .claude/skills/ path prefix with target platform prefix."""
return content.replace(source_prefix + '/', target_prefix + '/')
def switch_runtime_content(content, target_runtime):
"""Switch runtime invocations in .md content. Returns (new_content, switched)."""
if target_runtime == 'python':
new = RX_PS.sub(r'python \1.py', content)
elif target_runtime == 'powershell':
new = RX_PY.sub(r'powershell.exe -NoProfile -File \1.ps1', content)
else:
return content, False
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()
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
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def cmd_install(platform, runtime, project_dir):
"""Copy skills to target platform directory with path rewriting."""
src_dir = source_skills_dir()
target_prefix = PLATFORMS[platform]
target_dir = os.path.join(project_dir, target_prefix.replace('/', os.sep))
skills = scan_skills(src_dir)
if not skills:
print(f"Ошибка: навыки не найдены в {src_dir}", file=sys.stderr)
return 1
if os.path.isdir(target_dir):
existing = scan_skills(target_dir)
if existing:
print(f"В {target_prefix}/ уже есть {len(existing)} навыков. Обновляю...")
shutil.rmtree(target_dir)
os.makedirs(target_dir, exist_ok=True)
installed = 0
warnings = []
print(f"\nКопирование {len(skills)} навыков в {target_prefix}/ ...")
for skill_name in skills:
src_skill = os.path.join(src_dir, skill_name)
dst_skill = os.path.join(target_dir, skill_name)
# Copy entire skill directory
shutil.copytree(src_skill, dst_skill)
# Rewrite paths in all .md files
for md_path in collect_md_files(dst_skill):
with open(md_path, 'r', encoding='utf-8') as f:
content = f.read()
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})")
if new_content != content:
with open(md_path, 'w', encoding='utf-8') as f:
f.write(new_content)
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}")
return 0
def cmd_undo(platform, project_dir):
"""Remove installed skills for a platform."""
target_prefix = PLATFORMS[platform]
target_dir = os.path.join(project_dir, target_prefix.replace('/', os.sep))
if not os.path.isdir(target_dir):
print(f"Директория {target_prefix}/ не найдена — нечего удалять.")
return 0
skills = scan_skills(target_dir)
shutil.rmtree(target_dir)
# Clean up empty parent directories
parent = os.path.dirname(target_dir)
if os.path.isdir(parent) and not os.listdir(parent):
os.rmdir(parent)
print(f"Удалено: {target_prefix}/ ({len(skills)} навыков)")
return 0
def cmd_switch_runtime(runtime, project_dir):
"""Switch runtime in-place for skills in the current project."""
# Find skills directory: try all known platform dirs
skills_dir = None
platform_name = None
for name, prefix in PLATFORMS.items():
candidate = os.path.join(project_dir, prefix.replace('/', os.sep))
if os.path.isdir(candidate) and scan_skills(candidate):
skills_dir = candidate
platform_name = name
break
if not skills_dir:
print("Ошибка: не найдена директория навыков в текущем каталоге.", file=sys.stderr)
return 1
skills = scan_skills(skills_dir)
switched = 0
warnings = []
print(f"\nПереключение на {runtime} в {PLATFORMS[platform_name]}/ ...")
for skill_name in skills:
skill_path = os.path.join(skills_dir, skill_name)
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)
md_name = os.path.basename(md_path)
print(f" [OK] {skill_name}/{md_name}")
switched += 1
print(f"\nПереключено {switched} файлов на {runtime}.")
if warnings:
print(f"\nПредупреждения (отсутствующие файлы):")
for w in warnings:
print(w)
return 0
# ---------------------------------------------------------------------------
# Interactive mode
# ---------------------------------------------------------------------------
def ask_choice(prompt, options, default=1):
"""Ask user to choose from numbered options. Returns 1-based index."""
print(f"\n{prompt}")
for i, (label, hint) in enumerate(options, 1):
marker = "*" if i == default else " "
print(f" {marker}{i}. {label:<16} ({hint})")
while True:
try:
raw = input(f"\nВыбор [{default}]: ").strip()
if not raw:
return default
val = int(raw)
if 1 <= val <= len(options):
return val
print(f" Введите число от 1 до {len(options)}")
except ValueError:
print(f" Введите число от 1 до {len(options)}")
except (EOFError, KeyboardInterrupt):
print("\nОтмена.")
sys.exit(0)
def interactive_mode():
"""Run interactive setup wizard."""
print("Навыки 1С — настройка платформы")
print("=" * 31)
platform_options = [
("Claude Code", ".claude/skills/"),
("Cursor", ".cursor/skills/"),
("GitHub Copilot", ".github/skills/"),
("OpenAI Codex", ".codex/skills/"),
("Gemini CLI", ".gemini/skills/"),
("OpenCode", ".opencode/skills/"),
]
platform_keys = ['claude-code', 'cursor', 'copilot', 'codex', 'gemini', 'opencode']
choice = ask_choice("Для какой платформы настроить навыки?", platform_options)
platform = platform_keys[choice - 1]
# Check if already installed — offer update or remove
project_dir = os.getcwd()
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):
existing = scan_skills(target_dir)
if existing:
action_options = [
("Обновить", f"перезаписать {len(existing)} навыков"),
("Удалить", f"удалить {target_prefix}/"),
("Отмена", "ничего не делать"),
]
action = ask_choice(
f"В {target_prefix}/ уже есть {len(existing)} навыков.",
action_options
)
if action == 2:
return cmd_undo(platform, project_dir)
if action == 3:
print("Отмена.")
return 0
runtime_options = [
("PowerShell", "рекомендуется для Windows"),
("Python", "рекомендуется для Linux/Mac"),
]
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:
return cmd_install(platform, runtime, project_dir)
# ---------------------------------------------------------------------------
# CLI argument parsing
# ---------------------------------------------------------------------------
def main():
if len(sys.argv) == 1:
return interactive_mode()
parser = argparse.ArgumentParser(
description='Переключение навыков 1С между AI-платформами и рантаймами',
epilog='Примеры:\n'
' python scripts/switch.py cursor\n'
' python scripts/switch.py cursor --runtime python\n'
' python scripts/switch.py --undo cursor\n'
' python scripts/switch.py --runtime python\n',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument('platform', nargs='?', choices=list(PLATFORMS.keys()),
help='целевая платформа')
parser.add_argument('--runtime', choices=['python', 'powershell'],
help='рантайм скриптов (python или powershell)')
parser.add_argument('--undo', action='store_true',
help='удалить навыки для указанной платформы')
parser.add_argument('--project-dir', default=os.getcwd(),
help='путь к целевому проекту (по умолчанию: текущий каталог)')
args = parser.parse_args()
# --undo requires platform
if args.undo:
if not args.platform:
parser.error("--undo требует указания платформы")
if args.platform == 'claude-code':
parser.error("--undo не применим к claude-code (это исходная платформа)")
return cmd_undo(args.platform, args.project_dir)
# --runtime without platform = in-place switch
if args.runtime and not args.platform:
return cmd_switch_runtime(args.runtime, args.project_dir)
# platform specified
if args.platform:
if args.platform == 'claude-code':
if args.runtime:
return cmd_switch_runtime(args.runtime, args.project_dir)
else:
parser.error("для claude-code укажите --runtime python или --runtime powershell")
runtime = args.runtime or 'powershell'
return cmd_install(args.platform, runtime, args.project_dir)
# No args at all — shouldn't reach here due to len(sys.argv)==1 check
return interactive_mode()
if __name__ == '__main__':
sys.exit(main() or 0)