diff --git a/.gitignore b/.gitignore index a98857a..e3e1ec7 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,7 @@ repomix-output.xml .claude/ .opencode/ release-manifest.json + +# VIM files +.*.sw? +.*.un~ diff --git a/assets/oca_contributor.toml b/assets/oca_contributor.toml index 6d9031e..d953172 100644 --- a/assets/oca_contributor.toml +++ b/assets/oca_contributor.toml @@ -7,3 +7,4 @@ uv = [ "odoo-addons-path" ] system_packages = ["postgresql"] +skills = ["https://github.com/trobz/public-skills", "git@github.com:trobz/skills.git"] diff --git a/bootstrap.sh b/bootstrap.sh index 05a9ff7..e235e95 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -113,12 +113,28 @@ install_trobz_local() { echo "trobz_local installed (CLI: tlc)" } +# Install vercel-labs/skills (Claude Code skills) +install_vercel_skills() { + if ! command -v nvm &>/dev/null; then + echo "Installing nvm..." + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash + fi + if ! command -v npx &>/dev/null; then + echo "Installing node and related commands..." + . ~/.nvm/nvm.sh && nvm install --lts + fi + echo "Installing vercel-labs/skills..." + npx -y skills > /dev/null + echo "vercel-labs/skills installed" +} + # Main execution install_git install_gh install_uv setup_github_ssh install_trobz_local +install_vercel_skills echo "" echo "=== Bootstrap complete ===" @@ -128,6 +144,7 @@ echo " ✓ git" echo " ✓ gh (GitHub CLI)" echo " ✓ uv" echo " ✓ trobz_local (tlc command available)" +echo " ✓ nvm, node, and vercel-labs/skills (AI agent skills)" echo "" echo "Next steps:" echo "" diff --git a/trobz_local/assets/oca_contributor.toml b/trobz_local/assets/oca_contributor.toml index 6d9031e..d953172 100644 --- a/trobz_local/assets/oca_contributor.toml +++ b/trobz_local/assets/oca_contributor.toml @@ -7,3 +7,4 @@ uv = [ "odoo-addons-path" ] system_packages = ["postgresql"] +skills = ["https://github.com/trobz/public-skills", "git@github.com:trobz/skills.git"] diff --git a/trobz_local/installers.py b/trobz_local/installers.py index cce5efe..831d7d9 100644 --- a/trobz_local/installers.py +++ b/trobz_local/installers.py @@ -349,6 +349,102 @@ def install_npm_packages(packages: list[str], dry_run: bool = False) -> list: return run_tasks(tasks) +def _install_skill(progress: Progress, task_id: TaskID, skill: str, npx_path: str): + progress.update(task_id, description=f"Installing {skill}...", total=100, completed=0) + + try: + subprocess.run( # noqa: S603 + [npx_path, "-y", "skills", "add", "-g", "--all", skill], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + progress.update(task_id, description=f"[red]✗ Failed to install {skill}") + raise PackageInstallError(skill, e.stderr) from e + + progress.update(task_id, description=f"✓ {skill} installed.", completed=100) + + +def install_agent_skills(skills: list[str], dry_run: bool = False) -> list: + if not skills: + return [] + + npx_path = shutil.which("npx") + if not npx_path: + typer.secho( + "Error: npx is not installed. Please install Node.js first.", + fg=typer.colors.RED, + ) + return [TaskResult(name="npx-check", success=False, message="npx is not installed")] + + if dry_run: + typer.echo("\n[Skills - would be installed via npx skills install]") + for skill in skills: + typer.echo(f" - {skill}") + return [] + + typer.secho("\n--- Installing Skills ---", fg=typer.colors.BLUE, bold=True) + + tasks = [] + for skill in skills: + tasks.append({ + "name": skill, + "func": _install_skill, + "args": {"skill": skill, "npx_path": npx_path}, + }) + + return run_tasks(tasks) + + +def _update_skill(progress: Progress, task_id: TaskID, skill: str, npx_path: str): + progress.update(task_id, description=f"Updating {skill}...", total=100, completed=0) + + try: + subprocess.run( # noqa: S603 + [npx_path, "-y", "skills", "update", skill], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + progress.update(task_id, description=f"[red]✗ Failed to update {skill}") + raise PackageInstallError(skill, e.stderr) from e + + progress.update(task_id, description=f"✓ {skill} updated.", completed=100) + + +def update_agent_skills(skills: list[str], dry_run: bool = False) -> list: + if not skills: + return [] + + npx_path = shutil.which("npx") + if not npx_path: + typer.secho( + "Error: npx is not installed. Please install Node.js first.", + fg=typer.colors.RED, + ) + return [TaskResult(name="npx-check", success=False, message="npx is not installed")] + + if dry_run: + typer.echo("\n[Skills - would be updated via npx skills update]") + for skill in skills: + typer.echo(f" - {skill}") + return [] + + typer.secho("\n--- Updating Skills ---", fg=typer.colors.BLUE, bold=True) + + tasks = [] + for skill in skills: + tasks.append({ + "name": skill, + "func": _update_skill, + "args": {"skill": skill, "npx_path": npx_path}, + }) + + return run_tasks(tasks) + + def _install_uv_tool(progress: Progress, task_id: TaskID, tool: str, uv_path: str): progress.update(task_id, description=f"Installing {tool}...", total=100, completed=0) diff --git a/trobz_local/main.py b/trobz_local/main.py index da572ca..cc265aa 100644 --- a/trobz_local/main.py +++ b/trobz_local/main.py @@ -16,11 +16,13 @@ from .concurrency import TaskResult, run_tasks from .doctor import CheckStatus, run_doctor from .installers import ( + install_agent_skills, install_npm_packages, install_scripts, install_system_packages, install_uv_tools, setup_postgresql_repo, + update_agent_skills, ) from .postgres import ( check_postgres_running, @@ -443,6 +445,56 @@ def install_tools( typer.secho("\n✓ All tools installed successfully.", fg=typer.colors.GREEN) +@app.command() +def install_skills( + dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be installed without executing."), +): + """Install AI agent skills specified in the skills field of config.""" + config = get_config() + skills = config.get("tools", {}).get("skills", []) + + if not skills: + code_root = get_code_root() + typer.echo(f"No skills found in config. Add skills to [tools] section in {code_root}/config.toml") + return + + results = install_agent_skills(skills, dry_run) + + if not dry_run: + failed = [r for r in results if not r.success] + if failed: + typer.secho("\n--- Some skill(s) installations failed ---", fg=typer.colors.RED) + for r in failed: + typer.secho(f"✗ {r.name}: {r.message}", fg=typer.colors.RED) + raise typer.Exit(code=1) + typer.secho("\n✓ All skills installed successfully.", fg=typer.colors.GREEN) + + +@app.command() +def update_skills( + dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be updated without executing."), +): + """Update AI agent skills specified in the skills field of config.""" + config = get_config() + skills = config.get("tools", {}).get("skills", []) + + if not skills: + code_root = get_code_root() + typer.echo(f"No skills found in config. Add skills to [tools] section in {code_root}/config.toml") + return + + results = update_agent_skills(skills, dry_run) + + if not dry_run: + failed = [r for r in results if not r.success] + if failed: + typer.secho("\n--- Some skill(s) updates failed ---", fg=typer.colors.RED) + for r in failed: + typer.secho(f"✗ {r.name}: {r.message}", fg=typer.colors.RED) + raise typer.Exit(code=1) + typer.secho("\n✓ All skills updated successfully.", fg=typer.colors.GREEN) + + @app.command() def create_venvs(ctx: typer.Context): """ diff --git a/trobz_local/utils.py b/trobz_local/utils.py index b332070..e4fd2bb 100644 --- a/trobz_local/utils.py +++ b/trobz_local/utils.py @@ -182,6 +182,7 @@ class ToolsConfig(BaseModel): npm: list[str] = Field(default_factory=list) script: list[ScriptItem] = Field(default_factory=list) system_packages: list[str] = Field(default_factory=list) + skills: list[str] = Field(default_factory=list) @field_validator("uv") @classmethod