diff --git a/.claude/skills/.gitignore b/.claude/skills/.gitignore new file mode 100644 index 00000000..58200d4d --- /dev/null +++ b/.claude/skills/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/.claude/skills/web-test/.gitignore b/.claude/skills/web-test/.gitignore new file mode 100644 index 00000000..434a54c1 --- /dev/null +++ b/.claude/skills/web-test/.gitignore @@ -0,0 +1,4 @@ +scripts/node_modules/ +.browser-session.json +*.png +*.mp4 diff --git a/.gitignore b/.gitignore index 1db0de8e..33d83063 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 79d70f5f..51c4b81b 100644 --- a/README.md +++ b/README.md @@ -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 ` | diff --git a/scripts/switch.py b/scripts/switch.py index 22851ea2..547f2747 100644 --- a/scripts/switch.py +++ b/scripts/switch.py @@ -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)