From b1a7e414d02719733176d58b739cda9f3f82c513 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 7 May 2026 15:56:26 +0300 Subject: [PATCH] fix(switch): runtime conversion for ${CLAUDE_SKILL_DIR} paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the SKILL.md refactor, paths are wrapped in double quotes and contain ${CLAUDE_SKILL_DIR}. The legacy RX_PS/RX_PY regexes captured the leading quote into the path group and didn't accept '$', '{', '}' characters, breaking three places: - classify_skill_runtime: misdetected runtime since RX_PY didn't match python invocations of variable paths - check_missing_files: built file paths like '"${CLAUDE_SKILL_DIR}/...py' that never existed → false-positive missing → runtime switch skipped - switch_runtime_content: failed to convert PS->Py / Py->PS for skills using the new path format Fix: - Regexes now capture optional surrounding quote separately and accept any non-whitespace non-quote chars in the path - New helper expand_skill_path() resolves ${CLAUDE_SKILL_DIR} to the actual on-disk path for file existence checks (handles cross-skill references via ..// too) - check_missing_files derives skill_name from skill_dir to drive the expansion Verified via: python scripts/switch.py claude-code --project-dir --runtime python python scripts/switch.py claude-code --project-dir --runtime powershell python scripts/switch.py codex --project-dir All produce correct output with quotes preserved and cross-skill references resolved. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/switch.py | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/scripts/switch.py b/scripts/switch.py index f559fa17..5653af80 100644 --- a/scripts/switch.py +++ b/scripts/switch.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# switch.py v1.4 — Переключение навыков 1С между AI-платформами и рантаймами +# switch.py v1.5 — Переключение навыков 1С между AI-платформами и рантаймами # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """ Копирует (или создаёт ссылки на) навыки из .claude/skills/ на другие AI-платформы @@ -61,8 +61,10 @@ GITIGNORE_RECOMMENDATIONS = [ # --------------------------------------------------------------------------- # 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") +# Capture optional surrounding quote (group 'q') and bare path (group 'path'). +# Path matches non-whitespace non-quote chars to support ${CLAUDE_SKILL_DIR}/... +RX_PS = re.compile(r'powershell\.exe\s+(?:-NoProfile\s+)?-File\s+(?P["\']?)(?P[^"\s]+?)\.ps1(?P=q)?') +RX_PY = re.compile(r"python\s+(?P[\"']?)(?P[^\"\s]+?)\.py(?P=q)?") # --------------------------------------------------------------------------- @@ -153,23 +155,40 @@ def classify_skill_runtime(skill_dir): return 'ps' if has_ps else ('py' if has_py else 'none') +def expand_skill_path(path, skill_name, source_prefix=SOURCE_PREFIX): + """Expand ${CLAUDE_SKILL_DIR} placeholder to a path relative to source_prefix. + + ${CLAUDE_SKILL_DIR}/ -> // + ${CLAUDE_SKILL_DIR}/..// -> // + Anything else returned as-is (legacy literal path). + """ + var = '${CLAUDE_SKILL_DIR}/' + if not path.startswith(var): + return path + rest = path[len(var):] + if rest.startswith('../'): + return f'{source_prefix}/{rest[3:]}' + return f'{source_prefix}/{skill_name}/{rest}' + + 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 = [] + skill_name = os.path.basename(os.path.normpath(skill_dir)) 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' + for m in RX_PS.finditer(content): + py_path = expand_skill_path(m.group('path'), skill_name) + '.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' + for m in RX_PY.finditer(content): + ps1_path = expand_skill_path(m.group('path'), skill_name) + '.ps1' if not os.path.isfile(os.path.join(root, ps1_path)): missing.append(ps1_path) return missing @@ -205,9 +224,15 @@ def rewrite_paths(content, platform, target_prefix, skill_name): 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) + def to_py(m): + q = m.group('q') + return f"python {q}{m.group('path')}.py{q}" + new = RX_PS.sub(to_py, content) elif target_runtime == 'powershell': - new = RX_PY.sub(r'powershell.exe -NoProfile -File \1.ps1', content) + def to_ps(m): + q = m.group('q') + return f"powershell.exe -NoProfile -File {q}{m.group('path')}.ps1{q}" + new = RX_PY.sub(to_ps, content) else: return content, False return new, new != content