diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a73dea1..e214cc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,9 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v4 + - name: Check uv.lock is up to date + run: uv lock --check + - name: Install dependencies run: uv sync --all-extras diff --git a/parallel_web_tools/cli/skills.py b/parallel_web_tools/cli/skills.py index 9589fb8..6b469f4 100644 --- a/parallel_web_tools/cli/skills.py +++ b/parallel_web_tools/cli/skills.py @@ -86,7 +86,8 @@ def skills_list(output_json: bool) -> None: @click.option( "--project", is_flag=True, - help="Install to .agents/skills in detected project root (default is global install).", + help="Install into the detected project root — .agents/skills, plus .claude/skills " + "when Claude Code is present (default is global install).", ) @click.option( "--skill", @@ -139,7 +140,7 @@ def skills_install(project: bool, skill_names: tuple[str, ...], output_json: boo @click.option( "--project", is_flag=True, - help="Uninstall from .agents/skills in detected project root (default is global install).", + help="Uninstall from the detected project root's skill directories (default is global install).", ) @click.option("--json", "output_json", is_flag=True, help="Output as JSON") def skills_uninstall(project: bool, output_json: bool) -> None: @@ -171,7 +172,7 @@ def skills_uninstall(project: bool, output_json: bool) -> None: @click.option( "--project", is_flag=True, - help="Reinstall in .agents/skills in detected project root (default is global install).", + help="Reinstall in the detected project root's skill directories (default is global install).", ) @click.option( "--skill", diff --git a/parallel_web_tools/core/skills.py b/parallel_web_tools/core/skills.py index 29a293c..b71aef5 100644 --- a/parallel_web_tools/core/skills.py +++ b/parallel_web_tools/core/skills.py @@ -106,8 +106,14 @@ def resolve_install_dirs(project: bool, start: Path | None = None) -> list[Path] ``.agents/skills`` is the canonical cross-agent location and always comes first. Claude Code does not read it — it only discovers skills under ``.claude/skills`` — - so when a Claude Code configuration directory is present we install there too. - Agents that read both (Cursor, for example) de-duplicate by skill name. + so when Claude Code is present we install there too. Agents that read both + (Cursor, for example) de-duplicate by skill name. + + Global installs mirror into the Claude Code configuration directory when it + exists; its absence means no Claude Code on this machine. Project installs + mirror into ``/.claude/skills``, created if needed whenever the machine + has Claude Code — a repo missing ``.claude`` just hasn't stored Claude settings + yet, and skipping it would leave the skills invisible to Claude Code there. An explicit ``PARALLEL_SKILLS_GLOBAL_DIR`` override targets exactly one directory, on the assumption that a caller naming a path wants only that path written. @@ -116,9 +122,14 @@ def resolve_install_dirs(project: bool, start: Path | None = None) -> list[Path] if not project and os.environ.get(GLOBAL_SKILLS_DIR_ENV): return [canonical] - claude_config_dir = canonical.parent.parent / ".claude" if project else get_claude_config_dir() - if not claude_config_dir.is_dir(): - return [canonical] + if project: + claude_config_dir = canonical.parent.parent / ".claude" + if not claude_config_dir.is_dir() and not get_claude_config_dir().is_dir(): + return [canonical] + else: + claude_config_dir = get_claude_config_dir() + if not claude_config_dir.is_dir(): + return [canonical] return _dedupe_dirs([canonical, claude_config_dir / "skills"]) diff --git a/scripts/release.sh b/scripts/release.sh index 13e7fe9..0e7a630 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -96,6 +96,9 @@ update_version_files() { sedi "s/parallel-web-tools>=.*/parallel-web-tools>=$new_version/" \ "$PROJECT_ROOT/parallel_web_tools/integrations/bigquery/cloud_function/requirements.txt" + # 4. uv.lock (records this package's own version; stale locks dirty every working tree) + (cd "$PROJECT_ROOT" && uv lock) + # 5. npm/package.json sedi "s/\"version\": \".*\"/\"version\": \"$npm_version\"/" "$PROJECT_ROOT/npm/package.json" } @@ -161,7 +164,8 @@ git add \ pyproject.toml \ parallel_web_tools/__init__.py \ parallel_web_tools/integrations/bigquery/cloud_function/requirements.txt \ - npm/package.json + npm/package.json \ + uv.lock # Commit — if pre-commit hooks modify files (e.g. uv.lock), re-stage and retry if ! git commit -m "chore: bump version to $NEW_VERSION"; then @@ -171,7 +175,8 @@ if ! git commit -m "chore: bump version to $NEW_VERSION"; then parallel_web_tools/__init__.py \ tests/test_cli.py \ parallel_web_tools/integrations/bigquery/cloud_function/requirements.txt \ - npm/package.json + npm/package.json \ + uv.lock # Also stage any lock files updated by hooks git diff --name-only | xargs -r git add git commit -m "chore: bump version to $NEW_VERSION" diff --git a/tests/test_skills.py b/tests/test_skills.py index b124a09..c4178a9 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -110,7 +110,9 @@ def test_project_adds_claude_dir_when_present(self, tmp_path): project_root / ".claude" / "skills", ] - def test_project_ignores_claude_config_dir_env(self, monkeypatch, tmp_path): + def test_project_mirrors_locally_when_machine_has_claude(self, monkeypatch, tmp_path): + """A repo without .claude still gets one when Claude Code exists on the machine, + and the mirror stays project-local — never the global config directory.""" project_root = tmp_path / "repo" project_root.mkdir() (project_root / "pyproject.toml").write_text("[project]\nname='x'\n") @@ -118,6 +120,18 @@ def test_project_ignores_claude_config_dir_env(self, monkeypatch, tmp_path): global_claude.mkdir(parents=True) monkeypatch.setenv(skills.CLAUDE_CONFIG_DIR_ENV, str(global_claude)) + assert skills.resolve_install_dirs(project=True, start=project_root) == [ + project_root / ".agents" / "skills", + project_root / ".claude" / "skills", + ] + + def test_project_skips_claude_dir_when_machine_has_no_claude(self, monkeypatch, tmp_path): + monkeypatch.delenv(skills.CLAUDE_CONFIG_DIR_ENV, raising=False) + monkeypatch.setattr("parallel_web_tools.core.skills.Path.home", lambda: tmp_path / "home") + project_root = tmp_path / "repo" + project_root.mkdir() + (project_root / "pyproject.toml").write_text("[project]\nname='x'\n") + assert skills.resolve_install_dirs(project=True, start=project_root) == [project_root / ".agents" / "skills"] def test_symlinked_claude_dir_is_deduped(self, monkeypatch, tmp_path): diff --git a/uv.lock b/uv.lock index 87823d9..162b081 100644 --- a/uv.lock +++ b/uv.lock @@ -1674,7 +1674,7 @@ wheels = [ [[package]] name = "parallel-web-tools" -version = "0.8.1" +version = "0.8.2" source = { editable = "." } dependencies = [ { name = "click" },