Skill Installer v3.0 Overview Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao. When to Use This Skill - When the user mentions "instalar skill" or related topics - When the user mentions "install skill" or related topics - When the user mentions "registrar skill" or related topics - When the user mentions "nova skill" or related topics - When the user mentions "new skill" or related topics - When the user mentions "adicionar skill ao ecossistema" or related topics Do Not Use This Skill When…

, block, re.MULTILINE)\n if m:\n result[key] = m.group(1).strip()\n else:\n m2 = re.search(\n rf'^{key}:\\s*>-?\\s*\\n((?:\\s+.+\\n?)+)', block, re.MULTILINE\n )\n if m2:\n lines = m2.group(1).strip().split(\"\\n\")\n result[key] = \" \".join(line.strip() for line in lines)\n return result\n\n\n# ── Core Functions ─────────────────────────────────────────────────────────\n\ndef get_installed_skill_names() -> set:\n \"\"\"Get set of skill names already installed in the ecosystem.\"\"\"\n names = set()\n\n for base in INSTALLED_LOCATIONS:\n if not base.exists():\n continue\n for root, dirs, files in os.walk(base):\n depth = len(Path(root).relative_to(base).parts)\n if depth > 3:\n dirs.clear()\n continue\n\n # Skip certain directories\n skip = {\"agent-orchestrator\", \"skill-installer\", \".git\", \"__pycache__\", \"node_modules\"}\n if any(part in skip for part in Path(root).parts):\n continue\n\n if \"SKILL.md\" in files:\n skill_md = Path(root) / \"SKILL.md\"\n meta = parse_yaml_frontmatter(skill_md)\n name = meta.get(\"name\", Path(root).name)\n names.add(name.lower())\n\n return names\n\n\ndef find_skill_candidates(scan_locations: list[Path]) -> list[dict]:\n \"\"\"Find SKILL.md files in given locations.\"\"\"\n candidates = []\n seen_paths = set()\n installed_names = get_installed_skill_names()\n\n for base in scan_locations:\n if not base.exists():\n continue\n\n # Skip the skills root itself (those are already installed)\n try:\n base.resolve().relative_to(SKILLS_ROOT.resolve())\n continue\n except ValueError:\n pass\n\n for root, dirs, files in os.walk(base):\n root_path = Path(root)\n depth = len(root_path.relative_to(base).parts)\n\n if depth > MAX_SCAN_DEPTH:\n dirs.clear()\n continue\n\n # Skip heavy directories\n skip_dirs = {\".git\", \"__pycache__\", \"node_modules\", \".venv\", \"venv\", \".tox\"}\n dirs[:] = [d for d in dirs if d not in skip_dirs]\n\n if \"SKILL.md\" in files:\n skill_md = root_path / \"SKILL.md\"\n resolved = skill_md.resolve()\n\n # Skip if already seen\n if str(resolved) in seen_paths:\n continue\n seen_paths.add(str(resolved))\n\n # Skip if inside SKILLS_ROOT\n try:\n resolved.relative_to(SKILLS_ROOT.resolve())\n continue\n except ValueError:\n pass\n\n # Parse metadata\n meta = parse_yaml_frontmatter(skill_md)\n name = meta.get(\"name\", root_path.name)\n description = meta.get(\"description\", \"\")\n has_valid = bool(name and description)\n\n # Check if already installed\n already_installed = name.lower() in installed_names\n\n # Detect if in a workspace\n is_workspace = bool(WORKSPACE_PATTERN.match(str(root_path)))\n\n desc_str = str(description)\n\n # Get modification timestamp and size\n try:\n mtime = skill_md.stat().st_mtime\n mtime_str = datetime.fromtimestamp(mtime).strftime(\"%Y-%m-%d %H:%M\")\n except Exception:\n mtime = 0.0\n mtime_str = \"unknown\"\n\n # Calculate directory size\n total_size = 0\n file_count = 0\n for r2, _, f2 in os.walk(root_path):\n for ff in f2:\n try:\n total_size += os.path.getsize(os.path.join(r2, ff))\n file_count += 1\n except OSError:\n pass\n size_kb = round(total_size / 1024, 1)\n\n candidates.append({\n \"name\": name,\n \"source_path\": str(root_path),\n \"skill_md_path\": str(skill_md),\n \"already_installed\": already_installed,\n \"valid_frontmatter\": has_valid,\n \"description\": desc_str[:120] + (\"...\" if len(desc_str) > 120 else \"\"),\n \"version\": meta.get(\"version\", \"\"),\n \"is_workspace\": is_workspace,\n \"location_type\": _classify_location(root_path),\n \"last_modified\": mtime_str,\n \"last_modified_ts\": mtime,\n \"size_kb\": size_kb,\n \"file_count\": file_count,\n })\n\n return candidates\n\n\ndef _classify_location(path: Path) -> str:\n \"\"\"Classify where the skill was found.\"\"\"\n path_str = str(path).lower()\n\n if \"desktop\" in path_str:\n return \"desktop\"\n if \"download\" in path_str:\n return \"downloads\"\n if \"document\" in path_str:\n return \"documents\"\n if \"temp\" in path_str or \"tmp\" in path_str:\n return \"temp\"\n if \"workspace\" in path_str:\n return \"workspace\"\n return \"other\"\n\n\n# ── Main Logic ─────────────────────────────────────────────────────────────\n\ndef detect(paths: list[Path] = None, scan_all: bool = False) -> dict:\n \"\"\"\n Detect uninstalled skills.\n\n Args:\n paths: Specific paths to scan. If None, uses defaults.\n scan_all: If True, scan extended locations too.\n\n Returns:\n dict with candidates list and summary.\n \"\"\"\n scan_locations = []\n\n if paths:\n scan_locations.extend(paths)\n else:\n scan_locations.extend(DEFAULT_SCAN_LOCATIONS)\n\n if scan_all:\n scan_locations.extend(EXTENDED_SCAN_LOCATIONS)\n\n # Deduplicate\n seen = set()\n unique_locations = []\n for loc in scan_locations:\n resolved = str(loc.resolve())\n if resolved not in seen:\n seen.add(resolved)\n unique_locations.append(loc)\n\n candidates = find_skill_candidates(unique_locations)\n\n # Sort: uninstalled first, then by most recently modified\n candidates.sort(key=lambda c: (\n c[\"already_installed\"],\n -c.get(\"last_modified_ts\", 0),\n c[\"name\"].lower(),\n ))\n\n not_installed = [c for c in candidates if not c[\"already_installed\"]]\n already = [c for c in candidates if c[\"already_installed\"]]\n\n return {\n \"total_found\": len(candidates),\n \"not_installed\": len(not_installed),\n \"already_installed\": len(already),\n \"scanned_locations\": [str(p) for p in unique_locations if p.exists()],\n \"candidates\": candidates,\n }\n\n\n# ── CLI Entry Point ───────────────────────────────────────────────────────\n\ndef main():\n paths = []\n scan_all = \"--all\" in sys.argv\n\n if \"--path\" in sys.argv:\n idx = sys.argv.index(\"--path\")\n if idx + 1 \u003c len(sys.argv):\n p = Path(sys.argv[idx + 1]).resolve()\n if p.exists():\n paths.append(p)\n else:\n print(json.dumps({\n \"error\": f\"Path does not exist: {p}\",\n \"total_found\": 0,\n \"candidates\": [],\n }, indent=2))\n sys.exit(1)\n\n result = detect(paths=paths if paths else None, scan_all=scan_all)\n print(json.dumps(result, indent=2, ensure_ascii=False))\n\n # Exit with 0 if candidates found, 1 if none\n sys.exit(0 if result[\"total_found\"] > 0 else 1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10601,"content_sha256":"a67840d2a279fab253fd408397a4e9573a5172bc0768c444c3e86add25deda81"},{"filename":"scripts/install_skill.py","content":"#!/usr/bin/env python3\n\"\"\"\nSkill Installer v3.0 - Enterprise-grade installer with 11-step redundant workflow.\n\nDetects, validates, copies, registers, and verifies skills in the ecosystem\nwith maximum redundancy, safety, auto-repair, rollback, and rich diagnostics.\n\nUsage:\n python install_skill.py --source \"C:\\\\path\\\\to\\\\skill\"\n python install_skill.py --source \"C:\\\\path\" --name \"my-skill\"\n python install_skill.py --source \"C:\\\\path\" --force\n python install_skill.py --source \"C:\\\\path\" --dry-run\n python install_skill.py --detect\n python install_skill.py --detect --auto\n python install_skill.py --uninstall \"skill-name\"\n python install_skill.py --health\n python install_skill.py --health --repair\n python install_skill.py --rollback \"skill-name\"\n python install_skill.py --reinstall-all\n python install_skill.py --status\n python install_skill.py --log [N]\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport sys\nimport json\nimport shutil\nimport hashlib\nimport subprocess\nimport re\nfrom pathlib import Path\nfrom datetime import datetime\n\n# Add scripts directory to path for imports\nSCRIPT_DIR = Path(__file__).parent.resolve()\nsys.path.insert(0, str(SCRIPT_DIR))\n\nfrom validate_skill import validate, parse_yaml_frontmatter\nfrom detect_skills import detect\n\n# ── Configuration ──────────────────────────────────────────────────────────\n\nSKILLS_ROOT = Path(r\"C:\\Users\\renat\\skills\")\nCLAUDE_SKILLS = SKILLS_ROOT / \".claude\" / \"skills\"\nINSTALLER_DIR = SKILLS_ROOT / \"skill-installer\"\nDATA_DIR = INSTALLER_DIR / \"data\"\nBACKUPS_DIR = DATA_DIR / \"backups\"\nSTAGING_DIR = DATA_DIR / \"staging\"\nLOG_PATH = DATA_DIR / \"install_log.json\"\nSCAN_SCRIPT = SKILLS_ROOT / \"agent-orchestrator\" / \"scripts\" / \"scan_registry.py\"\nREGISTRY_PATH = SKILLS_ROOT / \"agent-orchestrator\" / \"data\" / \"registry.json\"\n\nMAX_BACKUPS_PER_SKILL = 5\nMAX_LOG_ENTRIES = 500 # Log rotation threshold\nVERSION = \"3.0.0\"\n\n\n# ── Console Colors ─────────────────────────────────────────────────────────\n\nclass _C:\n \"\"\"ANSI color codes for terminal output. Degrades gracefully on Windows.\"\"\"\n _enabled = hasattr(sys.stdout, \"isatty\") and sys.stdout.isatty()\n # Check if stdout can handle UTF-8 symbols\n _utf8 = False\n try:\n _utf8 = sys.stdout.encoding and sys.stdout.encoding.lower().replace(\"-\", \"\") in (\"utf8\", \"utf16\")\n except Exception:\n pass\n\n @staticmethod\n def _wrap(code: str, text: str) -> str:\n if _C._enabled:\n return f\"\\033[{code}m{text}\\033[0m\"\n return text\n\n @staticmethod\n def green(t: str) -> str: return _C._wrap(\"32\", t)\n @staticmethod\n def red(t: str) -> str: return _C._wrap(\"31\", t)\n @staticmethod\n def yellow(t: str) -> str: return _C._wrap(\"33\", t)\n @staticmethod\n def cyan(t: str) -> str: return _C._wrap(\"36\", t)\n @staticmethod\n def bold(t: str) -> str: return _C._wrap(\"1\", t)\n @staticmethod\n def dim(t: str) -> str: return _C._wrap(\"2\", t)\n\n # ASCII-safe symbols for Windows cp1252 compatibility\n OK = \"[OK]\"\n FAIL = \"[FAIL]\"\n WARN = \"[WARN]\"\n\n\ndef _step(n: int, total: int, msg: str):\n \"\"\"Print a step progress indicator.\"\"\"\n print(f\" {_C.cyan(f'[{n}/{total}]')} {msg}\")\n\n\ndef _ok(msg: str):\n print(f\" {_C.green(_C.OK)} {msg}\")\n\n\ndef _warn(msg: str):\n print(f\" {_C.yellow(_C.WARN)} {msg}\")\n\n\ndef _fail(msg: str):\n print(f\" {_C.red(_C.FAIL)} {msg}\")\n\n\n# ── Utility Functions ──────────────────────────────────────────────────────\n\ndef sanitize_name(name: str) -> str:\n \"\"\"Sanitize skill name: lowercase, hyphens, no spaces.\"\"\"\n name = name.strip().lower()\n name = name.replace(\" \", \"-\")\n name = name.replace(\"_\", \"-\")\n # Remove any chars that aren't alphanumeric or hyphens\n name = \"\".join(c for c in name if c.isalnum() or c == \"-\")\n # Remove leading/trailing hyphens and collapse multiples\n while \"--\" in name:\n name = name.replace(\"--\", \"-\")\n return name.strip(\"-\")\n\n\ndef md5_dir(path: Path, exclude_dirs: set = None) -> str:\n \"\"\"Compute combined MD5 hash of all files in a directory.\n\n Excludes backup/staging dirs and normalizes paths to forward slashes\n for cross-platform consistency.\n \"\"\"\n if exclude_dirs is None:\n exclude_dirs = {\"backups\", \"staging\", \".git\", \"__pycache__\", \"node_modules\", \".venv\"}\n\n h = hashlib.md5()\n for root, dirs, files in os.walk(path):\n # Filter out excluded directories\n dirs[:] = [d for d in dirs if d not in exclude_dirs]\n for f in sorted(files):\n fp = Path(root) / f\n try:\n # Normalize to forward slashes for consistent hashing\n rel = fp.relative_to(path).as_posix()\n h.update(rel.encode(\"utf-8\"))\n with open(fp, \"rb\") as fh:\n for chunk in iter(lambda: fh.read(8192), b\"\"):\n h.update(chunk)\n except Exception:\n pass\n return h.hexdigest()\n\n\ndef parse_version(ver: str) -> tuple:\n \"\"\"Parse a semver string into a comparable tuple.\n\n Examples: '1.0.0' -> (1,0,0), '2.1' -> (2,1,0), '' -> (0,0,0)\n \"\"\"\n if not ver:\n return (0, 0, 0)\n parts = re.findall(r'\\d+', str(ver))\n while len(parts) \u003c 3:\n parts.append(\"0\")\n try:\n return tuple(int(p) for p in parts[:3])\n except (ValueError, TypeError):\n return (0, 0, 0)\n\n\ndef compare_versions(installed: str, source: str) -> str:\n \"\"\"Compare two version strings.\n\n Returns: 'same', 'upgrade', 'downgrade', or 'unknown'.\n \"\"\"\n inst = parse_version(installed)\n src = parse_version(source)\n\n if inst == (0, 0, 0) or src == (0, 0, 0):\n return \"unknown\"\n if inst == src:\n return \"same\"\n if src > inst:\n return \"upgrade\"\n return \"downgrade\"\n\n\ndef load_log() -> list:\n \"\"\"Load install log.\"\"\"\n if LOG_PATH.exists():\n try:\n data = json.loads(LOG_PATH.read_text(encoding=\"utf-8\"))\n return data.get(\"operations\", [])\n except Exception:\n pass\n return []\n\n\ndef save_log(operations: list):\n \"\"\"Save install log with rotation (keeps last MAX_LOG_ENTRIES).\"\"\"\n DATA_DIR.mkdir(parents=True, exist_ok=True)\n # Rotate: keep only the last N entries\n if len(operations) > MAX_LOG_ENTRIES:\n operations = operations[-MAX_LOG_ENTRIES:]\n data = {\n \"version\": VERSION,\n \"operations\": operations,\n \"total_operations\": len(operations),\n \"last_updated\": datetime.now().isoformat(),\n }\n LOG_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding=\"utf-8\")\n\n\ndef append_log(entry: dict):\n \"\"\"Append entry to install log.\"\"\"\n ops = load_log()\n ops.append(entry)\n save_log(ops)\n\n\ndef cleanup_old_backups(skill_name: str):\n \"\"\"Keep only the last N backups for a skill.\"\"\"\n if not BACKUPS_DIR.exists():\n return\n\n prefix = f\"{skill_name}_\"\n backups = sorted(\n [d for d in BACKUPS_DIR.iterdir() if d.is_dir() and d.name.startswith(prefix)],\n key=lambda d: d.stat().st_mtime,\n )\n\n while len(backups) > MAX_BACKUPS_PER_SKILL:\n old = backups.pop(0)\n try:\n shutil.rmtree(old)\n except Exception:\n pass\n\n\ndef get_all_skill_dirs() -> list:\n \"\"\"Get all skill directories in the ecosystem (top-level + nested).\"\"\"\n dirs = []\n for item in sorted(SKILLS_ROOT.iterdir()):\n if not item.is_dir() or item.name.startswith(\".\"):\n continue\n if item.name == \"agent-orchestrator\":\n continue\n skill_md = item / \"SKILL.md\"\n if skill_md.exists():\n dirs.append(item)\n # Check nested (e.g., juntas-comerciais/junta-leiloeiros)\n for child in item.iterdir():\n if child.is_dir() and (child / \"SKILL.md\").exists():\n if child not in dirs:\n dirs.append(child)\n return dirs\n\n\n# ── Installation Steps ─────────────────────────────────────────────────────\n\ndef step1_resolve_source(source: str = None, do_detect: bool = False, auto: bool = False) -> dict:\n \"\"\"STEP 1: Resolve source directory.\"\"\"\n if source:\n source_path = Path(source).resolve()\n if not source_path.exists():\n return {\"success\": False, \"error\": f\"Source does not exist: {source_path}\"}\n if not (source_path / \"SKILL.md\").exists():\n return {\"success\": False, \"error\": f\"No SKILL.md found in {source_path}\"}\n return {\"success\": True, \"sources\": [str(source_path)]}\n\n if do_detect:\n result = detect()\n candidates = [c for c in result[\"candidates\"] if not c[\"already_installed\"]]\n\n if not candidates:\n return {\n \"success\": False,\n \"error\": \"No uninstalled skills detected\",\n \"scanned_locations\": result.get(\"scanned_locations\", []),\n }\n\n if auto:\n return {\n \"success\": True,\n \"sources\": [c[\"source_path\"] for c in candidates],\n \"candidates\": candidates,\n }\n\n # Return candidates for user to choose\n return {\n \"success\": True,\n \"sources\": [c[\"source_path\"] for c in candidates],\n \"candidates\": candidates,\n \"interactive\": True,\n }\n\n return {\"success\": False, \"error\": \"No --source or --detect provided\"}\n\n\ndef step2_validate(source_path: Path) -> dict:\n \"\"\"STEP 2: Validate the skill.\"\"\"\n result = validate(source_path)\n return result\n\n\ndef step3_determine_name(source_path: Path, name_override: str = None) -> str:\n \"\"\"STEP 3: Determine skill name.\"\"\"\n if name_override:\n return sanitize_name(name_override)\n\n meta = parse_yaml_frontmatter(source_path / \"SKILL.md\")\n name = meta.get(\"name\", source_path.name)\n return sanitize_name(name)\n\n\ndef step4_check_conflicts(skill_name: str) -> dict:\n \"\"\"STEP 4: Check for existing skill with same name.\"\"\"\n dest = SKILLS_ROOT / skill_name\n claude_dest = CLAUDE_SKILLS / skill_name\n\n conflicts = []\n if dest.exists():\n conflicts.append(str(dest))\n if claude_dest.exists():\n conflicts.append(str(claude_dest))\n\n return {\n \"has_conflicts\": len(conflicts) > 0,\n \"conflicts\": conflicts,\n \"destination\": str(dest),\n \"claude_destination\": str(claude_dest),\n }\n\n\ndef _backup_ignore(directory, contents):\n \"\"\"Ignore function for shutil.copytree to skip backup/staging dirs.\"\"\"\n ignored = set()\n dir_path = Path(directory)\n for item in contents:\n item_path = dir_path / item\n # Skip backup and staging directories to prevent recursion\n if item in (\"backups\", \"staging\") and dir_path.name == \"data\":\n ignored.add(item)\n # Skip .git and __pycache__\n if item in (\".git\", \"__pycache__\", \"node_modules\", \".venv\"):\n ignored.add(item)\n return ignored\n\n\ndef step5_backup(skill_name: str) -> dict:\n \"\"\"STEP 5: Backup existing skill before overwrite.\"\"\"\n dest = SKILLS_ROOT / skill_name\n timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n backup_name = f\"{skill_name}_{timestamp}\"\n backup_path = BACKUPS_DIR / backup_name\n\n BACKUPS_DIR.mkdir(parents=True, exist_ok=True)\n\n backed_up = []\n\n if dest.exists():\n try:\n shutil.copytree(dest, backup_path, ignore=_backup_ignore, dirs_exist_ok=True)\n backed_up.append(str(dest))\n except Exception as e:\n return {\"success\": False, \"error\": f\"Backup failed for {dest}: {e}\"}\n\n claude_dest = CLAUDE_SKILLS / skill_name\n if claude_dest.exists():\n claude_backup = backup_path / \".claude-registration\"\n claude_backup.mkdir(parents=True, exist_ok=True)\n try:\n shutil.copytree(claude_dest, claude_backup / skill_name, dirs_exist_ok=True)\n backed_up.append(str(claude_dest))\n except Exception as e:\n return {\"success\": False, \"error\": f\"Backup failed for {claude_dest}: {e}\"}\n\n # Cleanup old backups\n cleanup_old_backups(skill_name)\n\n return {\n \"success\": True,\n \"backup_path\": str(backup_path),\n \"backed_up\": backed_up,\n }\n\n\ndef step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:\n \"\"\"STEP 6: Copy to skills root via staging area.\"\"\"\n dest = SKILLS_ROOT / skill_name\n staging = STAGING_DIR / skill_name\n\n STAGING_DIR.mkdir(parents=True, exist_ok=True)\n\n # Clean staging\n if staging.exists():\n shutil.rmtree(staging)\n\n # Copy to staging first (skip backups/staging to prevent recursion)\n try:\n shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True)\n except Exception as e:\n return {\"success\": False, \"error\": f\"Copy to staging failed: {e}\"}\n\n # Validate staging copy\n staging_skill_md = staging / \"SKILL.md\"\n if not staging_skill_md.exists():\n shutil.rmtree(staging, ignore_errors=True)\n return {\"success\": False, \"error\": \"SKILL.md missing after copy to staging\"}\n\n # Verify hash matches\n source_hash = md5_dir(source_path)\n staging_hash = md5_dir(staging)\n if source_hash != staging_hash:\n shutil.rmtree(staging, ignore_errors=True)\n return {\n \"success\": False,\n \"error\": f\"Hash mismatch: source={source_hash} staging={staging_hash}\",\n }\n\n # Remove existing destination if exists\n if dest.exists():\n try:\n shutil.rmtree(dest)\n except Exception as e:\n shutil.rmtree(staging, ignore_errors=True)\n return {\"success\": False, \"error\": f\"Cannot remove existing destination: {e}\"}\n\n # Move staging to final destination\n try:\n shutil.move(str(staging), str(dest))\n except Exception as e:\n # Try copy + delete as fallback (cross-device moves)\n try:\n shutil.copytree(staging, dest, dirs_exist_ok=True)\n shutil.rmtree(staging, ignore_errors=True)\n except Exception as e2:\n shutil.rmtree(staging, ignore_errors=True)\n return {\"success\": False, \"error\": f\"Move failed: {e}, copy fallback failed: {e2}\"}\n\n return {\n \"success\": True,\n \"installed_to\": str(dest),\n \"hash\": source_hash,\n }\n\n\ndef step7_register_claude(skill_name: str) -> dict:\n \"\"\"STEP 7: Register in .claude/skills/ for native Claude Code discovery.\"\"\"\n source_skill_md = SKILLS_ROOT / skill_name / \"SKILL.md\"\n claude_dest_dir = CLAUDE_SKILLS / skill_name\n\n if not source_skill_md.exists():\n return {\"success\": False, \"error\": f\"SKILL.md not found at {source_skill_md}\"}\n\n claude_dest_dir.mkdir(parents=True, exist_ok=True)\n\n # Copy SKILL.md\n try:\n shutil.copy2(source_skill_md, claude_dest_dir / \"SKILL.md\")\n except Exception as e:\n return {\"success\": False, \"error\": f\"Failed to copy SKILL.md to Claude skills: {e}\"}\n\n # Also copy references/ if it exists (useful for Claude to read)\n refs_dir = SKILLS_ROOT / skill_name / \"references\"\n if refs_dir.exists():\n claude_refs = claude_dest_dir / \"references\"\n try:\n if claude_refs.exists():\n shutil.rmtree(claude_refs)\n shutil.copytree(refs_dir, claude_refs)\n except Exception:\n pass # Non-critical\n\n return {\n \"success\": True,\n \"registered_at\": str(claude_dest_dir),\n \"files_registered\": [\"SKILL.md\"] + (\n [\"references/\"] if refs_dir.exists() else []\n ),\n }\n\n\ndef step8_update_registry() -> dict:\n \"\"\"STEP 8: Run scan_registry.py to update orchestrator registry.\"\"\"\n if not SCAN_SCRIPT.exists():\n return {\n \"success\": False,\n \"error\": f\"scan_registry.py not found at {SCAN_SCRIPT}\",\n }\n\n try:\n result = subprocess.run(\n [\"python\", str(SCAN_SCRIPT), \"--force\"],\n capture_output=True,\n text=True,\n timeout=30,\n cwd=str(SKILLS_ROOT),\n )\n if result.returncode == 0:\n try:\n scan_output = json.loads(result.stdout)\n except json.JSONDecodeError:\n scan_output = {\"raw\": result.stdout[:500]}\n return {\"success\": True, \"scan_output\": scan_output}\n else:\n return {\n \"success\": False,\n \"error\": f\"scan_registry.py failed: {result.stderr[:500]}\",\n }\n except subprocess.TimeoutExpired:\n return {\"success\": False, \"error\": \"scan_registry.py timed out (30s)\"}\n except Exception as e:\n return {\"success\": False, \"error\": f\"Failed to run scan_registry.py: {e}\"}\n\n\ndef step9_verify(skill_name: str) -> dict:\n \"\"\"STEP 9: Verify installation is complete and correct.\"\"\"\n checks = []\n\n # Check 1: Skill directory exists\n dest = SKILLS_ROOT / skill_name\n checks.append({\n \"check\": \"skill_dir_exists\",\n \"pass\": dest.exists(),\n \"path\": str(dest),\n })\n\n # Check 2: SKILL.md exists and is readable\n skill_md = dest / \"SKILL.md\"\n skill_md_ok = False\n if skill_md.exists():\n try:\n text = skill_md.read_text(encoding=\"utf-8\")\n skill_md_ok = len(text) > 10\n except Exception:\n pass\n checks.append({\n \"check\": \"skill_md_readable\",\n \"pass\": skill_md_ok,\n \"path\": str(skill_md),\n })\n\n # Check 3: Frontmatter parseable\n meta = parse_yaml_frontmatter(skill_md) if skill_md.exists() else {}\n checks.append({\n \"check\": \"frontmatter_parseable\",\n \"pass\": bool(meta.get(\"name\")),\n \"name\": meta.get(\"name\", \"\"),\n })\n\n # Check 4: Claude Code registration\n claude_skill_md = CLAUDE_SKILLS / skill_name / \"SKILL.md\"\n checks.append({\n \"check\": \"claude_registered\",\n \"pass\": claude_skill_md.exists(),\n \"path\": str(claude_skill_md),\n })\n\n # Check 5: Appears in registry\n in_registry = False\n if REGISTRY_PATH.exists():\n try:\n registry = json.loads(REGISTRY_PATH.read_text(encoding=\"utf-8\"))\n skill_names = [s.get(\"name\", \"\").lower() for s in registry.get(\"skills\", [])]\n in_registry = skill_name.lower() in skill_names\n except Exception:\n pass\n checks.append({\n \"check\": \"in_registry\",\n \"pass\": in_registry,\n })\n\n all_passed = all(c[\"pass\"] for c in checks)\n\n return {\n \"success\": all_passed,\n \"checks\": checks,\n \"total\": len(checks),\n \"passed\": sum(1 for c in checks if c[\"pass\"]),\n \"failed\": sum(1 for c in checks if not c[\"pass\"]),\n }\n\n\ndef step10_log(skill_name: str, source: str, result: dict):\n \"\"\"STEP 10: Log the operation.\"\"\"\n entry = {\n \"timestamp\": datetime.now().isoformat(),\n \"action\": \"install\",\n \"skill_name\": skill_name,\n \"source\": source,\n \"destination\": str(SKILLS_ROOT / skill_name),\n \"registered\": result.get(\"registered\", False),\n \"registry_updated\": result.get(\"registry_updated\", False),\n \"backup_path\": result.get(\"backup_path\"),\n \"success\": result.get(\"success\", False),\n \"verification\": result.get(\"verification\", {}),\n \"warnings\": result.get(\"warnings\", []),\n }\n\n try:\n append_log(entry)\n except Exception:\n pass # Logging failure is non-critical\n\n return entry\n\n\n# ── Main Install Workflow ──────────────────────────────────────────────────\n\ndef install_single(\n source_path: str,\n name_override: str = None,\n force: bool = False,\n dry_run: bool = False,\n verbose: bool = True,\n) -> dict:\n \"\"\"Install a single skill through the 11-step workflow.\n\n Args:\n source_path: Path to skill directory containing SKILL.md.\n name_override: Optional name to use instead of frontmatter name.\n force: If True, overwrite existing skill (backup first).\n dry_run: If True, simulate all steps without writing anything.\n verbose: If True, print step-by-step progress to stdout.\n \"\"\"\n source = Path(source_path).resolve()\n total_steps = 11\n result = {\n \"success\": False,\n \"skill_name\": \"\",\n \"installed_to\": \"\",\n \"registered\": False,\n \"registry_updated\": False,\n \"backup_path\": None,\n \"warnings\": [],\n \"steps\": {},\n \"dry_run\": dry_run,\n \"installer_version\": VERSION,\n }\n\n if dry_run and verbose:\n print(f\"\\n{_C.bold(_C.yellow('=== DRY RUN MODE === No changes will be made'))}\\n\")\n\n # STEP 1: Already resolved (source is provided)\n if verbose:\n _step(1, total_steps, \"Resolving source...\")\n if not source.exists() or not (source / \"SKILL.md\").exists():\n result[\"error\"] = f\"Invalid source: {source}\"\n if verbose:\n _fail(f\"Source invalid: {source}\")\n return result\n\n result[\"steps\"][\"1_resolve\"] = {\"success\": True, \"source\": str(source)}\n if verbose:\n _ok(f\"Source: {source}\")\n\n # STEP 2: Validate\n if verbose:\n _step(2, total_steps, \"Validating skill...\")\n validation = step2_validate(source)\n result[\"steps\"][\"2_validate\"] = validation\n\n if not validation[\"valid\"]:\n result[\"error\"] = f\"Validation failed: {'; '.join(validation['errors'])}\"\n result[\"warnings\"] = validation.get(\"warnings\", [])\n if verbose:\n _fail(f\"Validation failed: {len(validation['errors'])} error(s)\")\n for e in validation[\"errors\"]:\n _fail(f\" {e}\")\n return result\n\n if verbose:\n _ok(f\"Validation passed ({validation['passed']}/{validation['total_checks']} checks)\")\n for w in validation.get(\"warnings\", []):\n _warn(f\" {w}\")\n\n result[\"warnings\"].extend(validation.get(\"warnings\", []))\n\n # STEP 3: Determine name\n if verbose:\n _step(3, total_steps, \"Determining skill name...\")\n skill_name = step3_determine_name(source, name_override)\n result[\"skill_name\"] = skill_name\n result[\"steps\"][\"3_name\"] = {\"name\": skill_name}\n\n if not skill_name:\n result[\"error\"] = \"Could not determine skill name\"\n if verbose:\n _fail(\"Could not determine skill name\")\n return result\n if verbose:\n _ok(f\"Name: {_C.bold(skill_name)}\")\n\n # Version comparison with installed\n source_meta = parse_yaml_frontmatter(source / \"SKILL.md\")\n source_version = source_meta.get(\"version\", \"\")\n dest = SKILLS_ROOT / skill_name\n if dest.exists() and (dest / \"SKILL.md\").exists():\n installed_meta = parse_yaml_frontmatter(dest / \"SKILL.md\")\n installed_version = installed_meta.get(\"version\", \"\")\n ver_cmp = compare_versions(installed_version, source_version)\n result[\"version_comparison\"] = {\n \"installed\": installed_version,\n \"source\": source_version,\n \"result\": ver_cmp,\n }\n if verbose and ver_cmp != \"unknown\":\n if ver_cmp == \"upgrade\":\n _ok(f\"Version: {installed_version} -> {_C.green(source_version)} (upgrade)\")\n elif ver_cmp == \"downgrade\":\n _warn(f\"Version: {installed_version} -> {_C.yellow(source_version)} (downgrade)\")\n elif ver_cmp == \"same\":\n _ok(f\"Version: {source_version} (same)\")\n\n # STEP 4: Check conflicts\n if verbose:\n _step(4, total_steps, \"Checking conflicts...\")\n conflicts = step4_check_conflicts(skill_name)\n result[\"steps\"][\"4_conflicts\"] = conflicts\n\n if conflicts[\"has_conflicts\"] and not force:\n result[\"error\"] = (\n f\"Skill '{skill_name}' already exists at: {', '.join(conflicts['conflicts'])}. \"\n f\"Use --force to overwrite.\"\n )\n if verbose:\n _fail(f\"Conflict: skill already exists. Use --force to overwrite.\")\n return result\n if verbose:\n if conflicts[\"has_conflicts\"]:\n _warn(f\"Conflict detected -- will overwrite (--force)\")\n else:\n _ok(\"No conflicts\")\n\n # STEP 5: Backup (if overwriting)\n if verbose:\n _step(5, total_steps, \"Creating backup...\")\n backup_result = {\"success\": True, \"backup_path\": None}\n if conflicts[\"has_conflicts\"] and force:\n if dry_run:\n backup_result = {\"success\": True, \"backup_path\": \"(dry-run)\", \"dry_run\": True}\n if verbose:\n _ok(\"Backup would be created (dry-run)\")\n else:\n backup_result = step5_backup(skill_name)\n if not backup_result[\"success\"]:\n result[\"error\"] = f\"Backup failed: {backup_result.get('error')}\"\n if verbose:\n _fail(f\"Backup failed: {backup_result.get('error')}\")\n return result\n result[\"backup_path\"] = backup_result.get(\"backup_path\")\n if verbose:\n _ok(f\"Backup saved: {backup_result.get('backup_path', '?')}\")\n else:\n if verbose:\n _ok(\"No backup needed (new install)\")\n\n result[\"steps\"][\"5_backup\"] = backup_result\n\n # Check idempotency: same content?\n idempotent = False\n if dest.exists():\n source_hash = md5_dir(source)\n dest_hash = md5_dir(dest)\n if source_hash == dest_hash:\n idempotent = True\n result[\"idempotent\"] = True\n result[\"installed_to\"] = str(dest)\n result[\"steps\"][\"6_copy\"] = {\n \"success\": True,\n \"installed_to\": str(dest),\n \"skipped\": \"identical content already at destination\",\n \"hash\": source_hash,\n }\n if verbose:\n _ok(\"Content identical -- skipping copy\")\n\n # STEP 6: Copy to skills root (skip if idempotent)\n if not idempotent:\n if verbose:\n _step(6, total_steps, \"Copying to skills root via staging...\")\n if dry_run:\n result[\"steps\"][\"6_copy\"] = {\n \"success\": True,\n \"installed_to\": str(dest),\n \"dry_run\": True,\n }\n result[\"installed_to\"] = str(dest)\n if verbose:\n _ok(f\"Would copy to: {dest} (dry-run)\")\n else:\n copy_result = step6_copy_to_skills_root(source, skill_name)\n result[\"steps\"][\"6_copy\"] = copy_result\n\n if not copy_result[\"success\"]:\n result[\"error\"] = f\"Copy failed: {copy_result.get('error')}\"\n if verbose:\n _fail(f\"Copy failed: {copy_result.get('error')}\")\n step10_log(skill_name, str(source), result)\n return result\n\n result[\"installed_to\"] = copy_result[\"installed_to\"]\n if verbose:\n _ok(f\"Copied to: {copy_result['installed_to']}\")\n elif verbose and not idempotent:\n _step(6, total_steps, \"Copying to skills root...\")\n\n # STEP 7: Register in Claude Code (ALWAYS runs, even if idempotent)\n if verbose:\n _step(7, total_steps, \"Registering in Claude Code CLI...\")\n if dry_run:\n result[\"steps\"][\"7_register\"] = {\"success\": True, \"dry_run\": True}\n result[\"registered\"] = True\n if verbose:\n _ok(\"Would register in .claude/skills/ (dry-run)\")\n else:\n register_result = step7_register_claude(skill_name)\n result[\"steps\"][\"7_register\"] = register_result\n result[\"registered\"] = register_result[\"success\"]\n\n if not register_result[\"success\"]:\n result[\"warnings\"].append(f\"Registration warning: {register_result.get('error')}\")\n if verbose:\n _warn(f\"Registration: {register_result.get('error')}\")\n elif verbose:\n _ok(f\"Registered at: {register_result.get('registered_at')}\")\n\n # STEP 8: Update orchestrator registry\n if verbose:\n _step(8, total_steps, \"Updating orchestrator registry...\")\n if dry_run:\n result[\"steps\"][\"8_registry\"] = {\"success\": True, \"dry_run\": True}\n result[\"registry_updated\"] = True\n if verbose:\n _ok(\"Would update registry (dry-run)\")\n else:\n registry_result = step8_update_registry()\n result[\"steps\"][\"8_registry\"] = registry_result\n result[\"registry_updated\"] = registry_result[\"success\"]\n\n if not registry_result[\"success\"]:\n result[\"warnings\"].append(f\"Registry update warning: {registry_result.get('error')}\")\n if verbose:\n _warn(f\"Registry: {registry_result.get('error')}\")\n elif verbose:\n _ok(\"Registry updated\")\n\n # STEP 9: Verify installation\n if verbose:\n _step(9, total_steps, \"Verifying installation...\")\n if dry_run:\n result[\"steps\"][\"9_verify\"] = {\"success\": True, \"dry_run\": True}\n result[\"verification\"] = {\"success\": True, \"dry_run\": True}\n if verbose:\n _ok(\"Verification skipped (dry-run)\")\n else:\n verify_result = step9_verify(skill_name)\n result[\"steps\"][\"9_verify\"] = verify_result\n result[\"verification\"] = verify_result\n if verbose:\n if verify_result[\"success\"]:\n _ok(f\"All {verify_result['total']} verification checks passed\")\n else:\n failed_checks = [c for c in verify_result[\"checks\"] if not c[\"pass\"]]\n _warn(f\"{verify_result['failed']}/{verify_result['total']} checks failed\")\n for c in failed_checks:\n _fail(f\" {c['check']}\")\n\n # STEP 10: Package ZIP for Claude.ai web upload\n if verbose:\n _step(10, total_steps, \"Packaging ZIP for Claude.ai...\")\n if dry_run:\n result[\"steps\"][\"10_package\"] = {\"success\": True, \"dry_run\": True}\n if verbose:\n _ok(\"Would create ZIP (dry-run)\")\n else:\n zip_result = {\"success\": False, \"skipped\": True}\n try:\n from package_skill import package_skill as pkg_skill\n zip_result = pkg_skill(SKILLS_ROOT / skill_name)\n result[\"steps\"][\"10_package\"] = zip_result\n result[\"zip_path\"] = zip_result.get(\"zip_path\") if zip_result[\"success\"] else None\n if verbose:\n if zip_result[\"success\"]:\n _ok(f\"ZIP: {zip_result.get('zip_path')} ({zip_result.get('zip_size_kb', '?')} KB)\")\n else:\n _warn(f\"ZIP: {zip_result.get('error', 'failed')}\")\n except Exception as e:\n zip_result = {\"success\": False, \"error\": str(e)}\n result[\"steps\"][\"10_package\"] = zip_result\n result[\"warnings\"].append(f\"ZIP packaging warning: {e}\")\n if verbose:\n _warn(f\"ZIP packaging: {e}\")\n\n # STEP 11: Log\n if verbose:\n _step(11, total_steps, \"Logging operation...\")\n if dry_run:\n result[\"success\"] = True\n result[\"steps\"][\"11_log\"] = {\"logged\": False, \"dry_run\": True}\n if verbose:\n _ok(\"Would log operation (dry-run)\")\n print(f\"\\n{_C.bold(_C.green('DRY RUN COMPLETE'))} -- no changes were made.\\n\")\n else:\n result[\"success\"] = result.get(\"verification\", {}).get(\"success\", False)\n if not result.get(\"verification\", {}).get(\"success\", True):\n failed_checks = [c for c in result.get(\"verification\", {}).get(\"checks\", []) if not c.get(\"pass\")]\n result[\"warnings\"].append(\n f\"Verification: {result['verification'].get('failed', 0)} check(s) failed: \"\n + \", \".join(c[\"check\"] for c in failed_checks)\n )\n\n log_entry = step10_log(skill_name, str(source), result)\n result[\"steps\"][\"11_log\"] = {\"logged\": True}\n if verbose:\n _ok(\"Operation logged\")\n if result[\"success\"]:\n print(f\"\\n{_C.bold(_C.green('SUCCESS'))} -- {_C.bold(skill_name)} installed.\\n\")\n else:\n print(f\"\\n{_C.bold(_C.red('FAILED'))} -- see warnings above.\\n\")\n\n return result\n\n\n# ── Uninstall ─────────────────────────────────────────────────────────────\n\ndef uninstall_skill(skill_name: str, keep_backup: bool = True) -> dict:\n \"\"\"Uninstall a skill: remove from skills root, .claude/skills/, and registry.\"\"\"\n skill_name = sanitize_name(skill_name)\n result = {\n \"success\": False,\n \"skill_name\": skill_name,\n \"removed\": [],\n \"backup_path\": None,\n }\n\n dest = SKILLS_ROOT / skill_name\n claude_dest = CLAUDE_SKILLS / skill_name\n\n if not dest.exists() and not claude_dest.exists():\n result[\"error\"] = f\"Skill '{skill_name}' not found in any location\"\n return result\n\n # Backup before removing\n if keep_backup and dest.exists():\n timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n backup_path = BACKUPS_DIR / f\"{skill_name}_{timestamp}\"\n BACKUPS_DIR.mkdir(parents=True, exist_ok=True)\n try:\n shutil.copytree(dest, backup_path, dirs_exist_ok=True)\n result[\"backup_path\"] = str(backup_path)\n except Exception as e:\n result[\"error\"] = f\"Backup failed: {e}\"\n return result\n\n # Remove from skills root\n if dest.exists():\n try:\n shutil.rmtree(dest)\n result[\"removed\"].append(str(dest))\n except Exception as e:\n result[\"error\"] = f\"Failed to remove {dest}: {e}\"\n return result\n\n # Remove from .claude/skills/\n if claude_dest.exists():\n try:\n shutil.rmtree(claude_dest)\n result[\"removed\"].append(str(claude_dest))\n except Exception as e:\n result[\"warnings\"] = [f\"Failed to remove Claude registration: {e}\"]\n\n # Update registry\n registry_result = step8_update_registry()\n\n # Remove ZIP from Desktop if exists\n zip_path = Path(os.path.expanduser(\"~\")) / \"Desktop\" / f\"{skill_name}.zip\"\n if zip_path.exists():\n try:\n zip_path.unlink()\n result[\"removed\"].append(str(zip_path))\n except Exception:\n pass\n\n # Log operation\n entry = {\n \"timestamp\": datetime.now().isoformat(),\n \"action\": \"uninstall\",\n \"skill_name\": skill_name,\n \"removed\": result[\"removed\"],\n \"backup_path\": result.get(\"backup_path\"),\n \"success\": True,\n }\n try:\n append_log(entry)\n except Exception:\n pass\n\n result[\"success\"] = True\n result[\"registry_updated\"] = registry_result.get(\"success\", False)\n return result\n\n\n# ── Health Check ──────────────────────────────────────────────────────────\n\ndef health_check() -> dict:\n \"\"\"Run a global health check on all installed skills.\"\"\"\n results = []\n\n # Load registry\n registry_skills = []\n if REGISTRY_PATH.exists():\n try:\n registry = json.loads(REGISTRY_PATH.read_text(encoding=\"utf-8\"))\n registry_skills = registry.get(\"skills\", [])\n except Exception:\n pass\n\n registry_names = {s.get(\"name\", \"\").lower() for s in registry_skills}\n\n # Check all skill directories in skills root\n for item in sorted(SKILLS_ROOT.iterdir()):\n if not item.is_dir():\n continue\n if item.name.startswith(\".\"):\n continue\n if item.name in (\"agent-orchestrator\", \"skill-installer\"):\n continue\n\n skill_md = item / \"SKILL.md\"\n if not skill_md.exists():\n continue\n\n meta = parse_yaml_frontmatter(skill_md)\n name = meta.get(\"name\", item.name).lower()\n\n checks = {\n \"name\": name,\n \"dir\": str(item),\n \"skill_md_exists\": skill_md.exists(),\n \"frontmatter_ok\": bool(meta.get(\"name\") and meta.get(\"description\")),\n \"claude_registered\": (CLAUDE_SKILLS / name / \"SKILL.md\").exists(),\n \"in_registry\": name in registry_names,\n \"has_scripts\": (item / \"scripts\").exists(),\n \"has_references\": (item / \"references\").exists(),\n }\n\n # Count issues\n issues = []\n if not checks[\"frontmatter_ok\"]:\n issues.append(\"invalid frontmatter (missing name or description)\")\n if not checks[\"claude_registered\"]:\n issues.append(\"not registered in .claude/skills/\")\n if not checks[\"in_registry\"]:\n issues.append(\"not in orchestrator registry\")\n\n checks[\"healthy\"] = len(issues) == 0\n checks[\"issues\"] = issues\n results.append(checks)\n\n # Also check nested skills (e.g., juntas-comerciais/junta-leiloeiros)\n for parent in SKILLS_ROOT.iterdir():\n if not parent.is_dir() or parent.name.startswith(\".\"):\n continue\n if parent.name in (\"agent-orchestrator\", \"skill-installer\"):\n continue\n for child in parent.iterdir():\n if child.is_dir() and (child / \"SKILL.md\").exists():\n # Skip if already checked at top level\n if any(r[\"dir\"] == str(child) for r in results):\n continue\n meta = parse_yaml_frontmatter(child / \"SKILL.md\")\n name = meta.get(\"name\", child.name).lower()\n checks = {\n \"name\": name,\n \"dir\": str(child),\n \"skill_md_exists\": True,\n \"frontmatter_ok\": bool(meta.get(\"name\") and meta.get(\"description\")),\n \"claude_registered\": (CLAUDE_SKILLS / name / \"SKILL.md\").exists(),\n \"in_registry\": name in registry_names,\n \"has_scripts\": (child / \"scripts\").exists(),\n \"has_references\": (child / \"references\").exists(),\n }\n issues = []\n if not checks[\"frontmatter_ok\"]:\n issues.append(\"invalid frontmatter\")\n if not checks[\"claude_registered\"]:\n issues.append(\"not registered in .claude/skills/\")\n if not checks[\"in_registry\"]:\n issues.append(\"not in orchestrator registry\")\n checks[\"healthy\"] = len(issues) == 0\n checks[\"issues\"] = issues\n results.append(checks)\n\n healthy = sum(1 for r in results if r[\"healthy\"])\n unhealthy = sum(1 for r in results if not r[\"healthy\"])\n\n # Check for registry duplicates\n from collections import Counter\n reg_name_counts = Counter(s.get(\"name\", \"\").lower() for s in registry_skills)\n duplicates = {name: count for name, count in reg_name_counts.items() if count > 1}\n\n return {\n \"total_skills\": len(results),\n \"healthy\": healthy,\n \"unhealthy\": unhealthy,\n \"registry_duplicates\": duplicates,\n \"skills\": results,\n }\n\n\n# ── Auto-Repair ──────────────────────────────────────────────────────────\n\ndef repair_health(verbose: bool = True) -> dict:\n \"\"\"Run health check and automatically fix all issues found.\n\n Fixes:\n - Skills not registered in .claude/skills/ -> registers them\n - Skills not in orchestrator registry -> triggers registry scan\n - Registry duplicates -> triggers re-scan with deduplication\n \"\"\"\n if verbose:\n print(f\"\\n{_C.bold('=== HEALTH CHECK + AUTO-REPAIR ===')}\\n\")\n\n health = health_check()\n repairs = []\n errors = []\n\n unhealthy_skills = [s for s in health[\"skills\"] if not s[\"healthy\"]]\n\n if not unhealthy_skills and not health[\"registry_duplicates\"]:\n if verbose:\n _ok(f\"All {health['total_skills']} skills are healthy. Nothing to repair.\")\n health[\"repairs\"] = []\n return health\n\n # Fix: register missing skills in .claude/skills/\n for skill in unhealthy_skills:\n if \"not registered in .claude/skills/\" in \"; \".join(skill[\"issues\"]):\n name = skill[\"name\"]\n skill_dir = Path(skill[\"dir\"])\n skill_md = skill_dir / \"SKILL.md\"\n if skill_md.exists():\n claude_dest = CLAUDE_SKILLS / name\n if verbose:\n _step(1, 2, f\"Registering '{name}' in .claude/skills/...\")\n try:\n claude_dest.mkdir(parents=True, exist_ok=True)\n shutil.copy2(skill_md, claude_dest / \"SKILL.md\")\n # Also copy references/ if present\n refs = skill_dir / \"references\"\n if refs.exists():\n claude_refs = claude_dest / \"references\"\n if claude_refs.exists():\n shutil.rmtree(claude_refs)\n shutil.copytree(refs, claude_refs)\n repairs.append({\"skill\": name, \"action\": \"registered\", \"success\": True})\n if verbose:\n _ok(f\"Registered: {name}\")\n except Exception as e:\n errors.append({\"skill\": name, \"action\": \"register\", \"error\": str(e)})\n if verbose:\n _fail(f\"Failed to register {name}: {e}\")\n\n # Fix: update registry to pick up missing skills and remove duplicates\n needs_registry_update = (\n any(\"not in orchestrator registry\" in \"; \".join(s[\"issues\"]) for s in unhealthy_skills)\n or health[\"registry_duplicates\"]\n )\n if needs_registry_update:\n if verbose:\n _step(2, 2, \"Updating orchestrator registry...\")\n reg_result = step8_update_registry()\n if reg_result[\"success\"]:\n repairs.append({\"action\": \"registry_update\", \"success\": True})\n if verbose:\n _ok(\"Registry updated\")\n else:\n errors.append({\"action\": \"registry_update\", \"error\": reg_result.get(\"error\")})\n if verbose:\n _fail(f\"Registry update failed: {reg_result.get('error')}\")\n\n # Re-run health check to confirm\n health_after = health_check()\n\n result = {\n \"before\": {\n \"healthy\": health[\"healthy\"],\n \"unhealthy\": health[\"unhealthy\"],\n \"duplicates\": len(health[\"registry_duplicates\"]),\n },\n \"after\": {\n \"healthy\": health_after[\"healthy\"],\n \"unhealthy\": health_after[\"unhealthy\"],\n \"duplicates\": len(health_after[\"registry_duplicates\"]),\n },\n \"repairs\": repairs,\n \"errors\": errors,\n \"skills\": health_after[\"skills\"],\n }\n\n if verbose:\n fixed = health[\"unhealthy\"] - health_after[\"unhealthy\"]\n print(f\"\\n{_C.bold('Result:')} Fixed {_C.green(str(fixed))} of {health['unhealthy']} issues.\")\n if health_after[\"unhealthy\"] > 0:\n _warn(f\"{health_after['unhealthy']} issues remaining\")\n else:\n _ok(\"All skills healthy!\")\n print()\n\n return result\n\n\n# ── Rollback ─────────────────────────────────────────────────────────────\n\ndef rollback_skill(skill_name: str, verbose: bool = True) -> dict:\n \"\"\"Restore a skill from its latest backup.\n\n Finds the most recent backup for the given skill and restores it\n to the skills root, re-registers, and updates the registry.\n \"\"\"\n skill_name = sanitize_name(skill_name)\n result = {\n \"success\": False,\n \"skill_name\": skill_name,\n \"restored_from\": None,\n }\n\n if not BACKUPS_DIR.exists():\n result[\"error\"] = \"No backups directory found\"\n if verbose:\n _fail(\"No backups directory found\")\n return result\n\n # Find backups for this skill\n prefix = f\"{skill_name}_\"\n backups = sorted(\n [d for d in BACKUPS_DIR.iterdir() if d.is_dir() and d.name.startswith(prefix)],\n key=lambda d: d.stat().st_mtime,\n reverse=True,\n )\n\n if not backups:\n result[\"error\"] = f\"No backups found for skill '{skill_name}'\"\n if verbose:\n _fail(f\"No backups found for '{skill_name}'\")\n # Show available backups\n all_backups = [d.name for d in BACKUPS_DIR.iterdir() if d.is_dir()]\n if all_backups:\n print(f\" Available backups: {', '.join(sorted(set(b.rsplit('_', 2)[0] for b in all_backups)))}\")\n return result\n\n latest_backup = backups[0]\n backup_skill_md = latest_backup / \"SKILL.md\"\n\n if not backup_skill_md.exists():\n result[\"error\"] = f\"Backup is invalid (no SKILL.md): {latest_backup}\"\n if verbose:\n _fail(f\"Backup invalid: {latest_backup}\")\n return result\n\n if verbose:\n timestamp = latest_backup.name.replace(f\"{skill_name}_\", \"\")\n print(f\"\\n{_C.bold(f'=== ROLLBACK: {skill_name} ===')}\")\n print(f\" Backup: {latest_backup.name} ({timestamp})\")\n\n # Restore to skills root\n dest = SKILLS_ROOT / skill_name\n if verbose:\n _step(1, 3, \"Restoring from backup...\")\n\n try:\n if dest.exists():\n shutil.rmtree(dest)\n shutil.copytree(latest_backup, dest, ignore=_backup_ignore, dirs_exist_ok=True)\n result[\"restored_from\"] = str(latest_backup)\n if verbose:\n _ok(f\"Restored to: {dest}\")\n except Exception as e:\n result[\"error\"] = f\"Restore failed: {e}\"\n if verbose:\n _fail(f\"Restore failed: {e}\")\n return result\n\n # Re-register in Claude Code\n if verbose:\n _step(2, 3, \"Re-registering...\")\n reg = step7_register_claude(skill_name)\n if verbose:\n if reg[\"success\"]:\n _ok(\"Registered\")\n else:\n _warn(f\"Registration: {reg.get('error')}\")\n\n # Update registry\n if verbose:\n _step(3, 3, \"Updating registry...\")\n step8_update_registry()\n if verbose:\n _ok(\"Registry updated\")\n\n # Log operation\n append_log({\n \"timestamp\": datetime.now().isoformat(),\n \"action\": \"rollback\",\n \"skill_name\": skill_name,\n \"backup_used\": str(latest_backup),\n \"success\": True,\n })\n\n result[\"success\"] = True\n if verbose:\n print(f\"\\n{_C.bold(_C.green('ROLLBACK COMPLETE'))}\\n\")\n return result\n\n\n# ── Reinstall All ────────────────────────────────────────────────────────\n\ndef reinstall_all(force: bool = True, verbose: bool = True) -> dict:\n \"\"\"Re-register every installed skill in one pass.\n\n Iterates all skill directories, re-copies SKILL.md to .claude/skills/,\n re-packages ZIPs, and updates the registry.\n \"\"\"\n if verbose:\n print(f\"\\n{_C.bold('=== REINSTALL ALL SKILLS ===')}\\n\")\n\n skill_dirs = get_all_skill_dirs()\n results_list = []\n\n for i, skill_dir in enumerate(skill_dirs, 1):\n meta = parse_yaml_frontmatter(skill_dir / \"SKILL.md\")\n name = meta.get(\"name\", skill_dir.name)\n name = sanitize_name(name)\n\n if verbose:\n print(f\" [{i}/{len(skill_dirs)}] {_C.bold(name)}...\")\n\n # Re-register in .claude/skills/\n reg = step7_register_claude(name)\n\n # Re-package ZIP\n zip_result = {\"success\": False}\n try:\n from package_skill import package_skill as pkg_skill\n zip_result = pkg_skill(skill_dir)\n except Exception:\n pass\n\n r = {\n \"skill\": name,\n \"registered\": reg[\"success\"],\n \"zipped\": zip_result.get(\"success\", False),\n }\n results_list.append(r)\n\n if verbose:\n status = _C.green(_C.OK) if reg[\"success\"] else _C.red(_C.FAIL)\n zip_status = _C.green(\"ZIP-OK\") if zip_result.get(\"success\") else _C.yellow(\"ZIP-WARN\")\n print(f\" {status} registered {zip_status}\")\n\n # Final registry update\n if verbose:\n print(f\"\\n Updating registry...\")\n step8_update_registry()\n\n registered_ok = sum(1 for r in results_list if r[\"registered\"])\n zipped_ok = sum(1 for r in results_list if r[\"zipped\"])\n\n result = {\n \"total\": len(results_list),\n \"registered\": registered_ok,\n \"zipped\": zipped_ok,\n \"results\": results_list,\n }\n\n if verbose:\n print(f\"\\n{_C.bold('Result:')} {registered_ok}/{len(results_list)} registered, {zipped_ok}/{len(results_list)} zipped.\")\n print()\n\n # Log\n append_log({\n \"timestamp\": datetime.now().isoformat(),\n \"action\": \"reinstall_all\",\n \"total\": len(results_list),\n \"registered\": registered_ok,\n \"zipped\": zipped_ok,\n \"success\": True,\n })\n\n return result\n\n\n# ── Status Dashboard ─────────────────────────────────────────────────────\n\ndef show_status(verbose: bool = True) -> dict:\n \"\"\"Rich status dashboard showing all skills, versions, and health.\"\"\"\n health = health_check()\n\n # Load registry for version info\n registry_skills = {}\n if REGISTRY_PATH.exists():\n try:\n reg = json.loads(REGISTRY_PATH.read_text(encoding=\"utf-8\"))\n for s in reg.get(\"skills\", []):\n registry_skills[s.get(\"name\", \"\").lower()] = s\n except Exception:\n pass\n\n # Count backups per skill\n backup_counts = {}\n if BACKUPS_DIR.exists():\n for d in BACKUPS_DIR.iterdir():\n if d.is_dir():\n # Extract skill name (everything before last _TIMESTAMP)\n parts = d.name.rsplit(\"_\", 2)\n if len(parts) >= 3:\n bname = parts[0]\n else:\n bname = d.name\n backup_counts[bname] = backup_counts.get(bname, 0) + 1\n\n # Log stats\n log_ops = load_log()\n install_count = sum(1 for o in log_ops if o.get(\"action\") == \"install\")\n uninstall_count = sum(1 for o in log_ops if o.get(\"action\") == \"uninstall\")\n rollback_count = sum(1 for o in log_ops if o.get(\"action\") == \"rollback\")\n\n if verbose:\n print(f\"\\n{_C.bold('+' + '='*62 + '+')}\")\n print(f\"{_C.bold('|')} {_C.bold(_C.cyan('Skill Installer v' + VERSION + ' -- Status Dashboard'))} {_C.bold('|')}\")\n print(f\"{_C.bold('+' + '='*62 + '+')}\\n\")\n\n # Skills table header\n print(f\" {'Name':\u003c24} {'Version':\u003c10} {'Health':\u003c10} {'Registered':\u003c12} {'Backups':\u003c8}\")\n print(f\" {'-'*24} {'-'*10} {'-'*10} {'-'*12} {'-'*8}\")\n\n for skill in health[\"skills\"]:\n name = skill[\"name\"][:22]\n reg_entry = registry_skills.get(skill[\"name\"], {})\n version = reg_entry.get(\"version\", \"-\") or \"-\"\n status = _C.green(\"OK\") if skill[\"healthy\"] else _C.red(\"ISSUE\")\n registered = _C.green(\"Yes\") if skill[\"claude_registered\"] else _C.red(\"No\")\n backups = str(backup_counts.get(skill[\"name\"], 0))\n print(f\" {name:\u003c24} {version:\u003c10} {status:\u003c19} {registered:\u003c21} {backups:\u003c8}\")\n\n if not skill[\"healthy\"]:\n for issue in skill[\"issues\"]:\n print(f\" {_C.dim(f' -> {issue}')}\")\n\n print(f\"\\n {_C.bold('Summary:')}\")\n print(f\" Skills: {_C.bold(str(health['total_skills']))} total, \"\n f\"{_C.green(str(health['healthy']))} healthy, \"\n f\"{_C.red(str(health['unhealthy'])) if health['unhealthy'] else '0'} unhealthy\")\n if health[\"registry_duplicates\"]:\n print(f\" {_C.yellow('Duplicates:')} {health['registry_duplicates']}\")\n\n print(f\"\\n {_C.bold('Operations Log:')}\")\n print(f\" Installs: {install_count} | Uninstalls: {uninstall_count} | Rollbacks: {rollback_count}\")\n print(f\" Total logged: {len(log_ops)}\")\n print()\n\n return {\n \"health\": health,\n \"backup_counts\": backup_counts,\n \"log_stats\": {\n \"total\": len(log_ops),\n \"installs\": install_count,\n \"uninstalls\": uninstall_count,\n \"rollbacks\": rollback_count,\n },\n }\n\n\n# ── Log Viewer ───────────────────────────────────────────────────────────\n\ndef show_log(n: int = 20, verbose: bool = True) -> list:\n \"\"\"Show the last N log entries.\"\"\"\n ops = load_log()\n recent = ops[-n:] if len(ops) > n else ops\n\n if verbose:\n print(f\"\\n{_C.bold(f'=== Last {len(recent)} Operations ===')}\\n\")\n for op in reversed(recent):\n ts = op.get(\"timestamp\", \"?\")[:19]\n action = op.get(\"action\", \"?\")\n name = op.get(\"skill_name\", \"?\")\n success = op.get(\"success\", False)\n\n # Color the action\n if action == \"install\":\n action_str = _C.green(\"INSTALL\")\n elif action == \"uninstall\":\n action_str = _C.red(\"UNINSTALL\")\n elif action == \"rollback\":\n action_str = _C.yellow(\"ROLLBACK\")\n elif action == \"reinstall_all\":\n action_str = _C.cyan(\"REINSTALL-ALL\")\n else:\n action_str = action.upper()\n\n status = _C.green(_C.OK) if success else _C.red(_C.FAIL)\n print(f\" {_C.dim(ts)} {action_str:\u003c22} {name:\u003c24} {status}\")\n\n print()\n\n return recent\n\n\n# ── CLI Entry Point ───────────────────────────────────────────────────────\n\ndef main():\n args = sys.argv[1:]\n\n source = None\n name_override = None\n force = \"--force\" in args\n dry_run = \"--dry-run\" in args\n do_detect = \"--detect\" in args\n auto = \"--auto\" in args\n do_uninstall = \"--uninstall\" in args\n do_health = \"--health\" in args\n do_repair = \"--repair\" in args\n do_rollback = \"--rollback\" in args\n do_reinstall_all = \"--reinstall-all\" in args\n do_status = \"--status\" in args\n do_log = \"--log\" in args\n json_output = \"--json\" in args\n\n if \"--source\" in args:\n idx = args.index(\"--source\")\n if idx + 1 \u003c len(args):\n source = args[idx + 1]\n\n if \"--name\" in args:\n idx = args.index(\"--name\")\n if idx + 1 \u003c len(args):\n name_override = args[idx + 1]\n\n # ── Status dashboard ──\n if do_status:\n result = show_status(verbose=not json_output)\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0)\n\n # ── Log viewer ──\n if do_log:\n n = 20\n idx = args.index(\"--log\")\n if idx + 1 \u003c len(args):\n try:\n n = int(args[idx + 1])\n except ValueError:\n pass\n result = show_log(n=n, verbose=not json_output)\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0)\n\n # ── Health check (with optional auto-repair) ──\n if do_health:\n if do_repair:\n result = repair_health(verbose=not json_output)\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n remaining = result.get(\"after\", {}).get(\"unhealthy\", 0)\n sys.exit(0 if remaining == 0 else 1)\n else:\n result = health_check()\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n else:\n # Pretty print health\n print(f\"\\n{_C.bold('=== HEALTH CHECK ===')}\\n\")\n for s in result[\"skills\"]:\n if s[\"healthy\"]:\n _ok(s[\"name\"])\n else:\n _fail(f\"{s['name']}: {'; '.join(s['issues'])}\")\n print(f\"\\n {_C.bold(str(result['healthy']))}/{result['total_skills']} healthy\")\n if result[\"unhealthy\"] > 0:\n print(f\" {_C.yellow('Tip:')} run with --repair to auto-fix issues\")\n if result[\"registry_duplicates\"]:\n print(f\" {_C.yellow('Duplicates:')} {result['registry_duplicates']}\")\n print()\n sys.exit(0 if result[\"unhealthy\"] == 0 else 1)\n\n # ── Rollback ──\n if do_rollback:\n idx = args.index(\"--rollback\")\n if idx + 1 >= len(args):\n print(json.dumps({\"error\": \"Usage: --rollback \u003cskill-name>\"}, indent=2))\n sys.exit(1)\n skill_name = args[idx + 1]\n result = rollback_skill(skill_name, verbose=not json_output)\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0 if result[\"success\"] else 1)\n\n # ── Reinstall all ──\n if do_reinstall_all:\n result = reinstall_all(force=True, verbose=not json_output)\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0)\n\n # ── Uninstall ──\n if do_uninstall:\n idx = args.index(\"--uninstall\")\n if idx + 1 >= len(args):\n print(json.dumps({\"error\": \"Usage: --uninstall \u003cskill-name>\"}, indent=2))\n sys.exit(1)\n skill_name = args[idx + 1]\n result = uninstall_skill(skill_name)\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0 if result[\"success\"] else 1)\n\n # ── No arguments: show usage ──\n if not source and not do_detect:\n print(f\"\\n{_C.bold(_C.cyan('Skill Installer v' + VERSION))}\\n\")\n print(f\" {_C.bold('Install:')}\")\n print(f\" --source \u003cpath> Install skill from path\")\n print(f\" --source \u003cpath> --force Overwrite if exists\")\n print(f\" --source \u003cpath> --name \u003cname> Custom name override\")\n print(f\" --source \u003cpath> --dry-run Simulate without changes\")\n print(f\" --detect Auto-detect uninstalled skills\")\n print(f\" --detect --auto Detect and install all\")\n print(f\"\")\n print(f\" {_C.bold('Manage:')}\")\n print(f\" --uninstall \u003cname> Uninstall (with backup)\")\n print(f\" --rollback \u003cname> Restore from latest backup\")\n print(f\" --reinstall-all Re-register + re-package all skills\")\n print(f\"\")\n print(f\" {_C.bold('Monitor:')}\")\n print(f\" --health Health check all skills\")\n print(f\" --health --repair Health check + auto-fix issues\")\n print(f\" --status Rich status dashboard\")\n print(f\" --log [N] Show last N operations (default: 20)\")\n print(f\"\")\n print(f\" {_C.bold('Flags:')}\")\n print(f\" --json Output JSON instead of pretty text\")\n print(f\" --force Force overwrite\")\n print(f\" --dry-run Simulate without changes\")\n print()\n sys.exit(1)\n\n # ── Install from source ──\n if source:\n result = install_single(source, name_override, force, dry_run=dry_run, verbose=not json_output)\n if json_output:\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0 if result[\"success\"] else 1)\n\n # ── Detection mode ──\n elif do_detect:\n resolve = step1_resolve_source(do_detect=True, auto=auto)\n\n if not resolve[\"success\"]:\n print(json.dumps(resolve, indent=2, ensure_ascii=False))\n sys.exit(1)\n\n if resolve.get(\"interactive\") and not auto:\n if json_output:\n print(json.dumps({\n \"mode\": \"interactive\",\n \"message\": \"Skills detected but not installed.\",\n \"candidates\": resolve[\"candidates\"],\n }, indent=2, ensure_ascii=False))\n else:\n print(f\"\\n{_C.bold('=== Detected Uninstalled Skills ===')}\\n\")\n for i, c in enumerate(resolve[\"candidates\"], 1):\n name = c.get(\"name\", \"?\")\n src = c.get(\"source_path\", \"?\")\n loc = c.get(\"location_type\", \"?\")\n valid = _C.green(_C.OK) if c.get(\"valid_frontmatter\") else _C.red(_C.FAIL)\n print(f\" {i}. {_C.bold(name)} {valid}\")\n print(f\" {_C.dim(src)} ({loc})\")\n print(f\"\\n Run with --auto to install all, or --source \u003cpath> to install one.\\n\")\n sys.exit(0)\n\n # Auto mode: install all candidates\n results = []\n for src in resolve[\"sources\"]:\n r = install_single(src, force=force, dry_run=dry_run, verbose=not json_output)\n results.append(r)\n\n total = len(results)\n success = sum(1 for r in results if r[\"success\"])\n failed = total - success\n\n summary = {\n \"mode\": \"auto\",\n \"total\": total,\n \"success\": success,\n \"failed\": failed,\n \"results\": results,\n }\n\n if json_output:\n print(json.dumps(summary, indent=2, ensure_ascii=False))\n sys.exit(0 if failed == 0 else 1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":62199,"content_sha256":"cb94865e9b2ecf10d4e18a583bbed9cb811acd5b001d9b81be4c3af6738a7c93"},{"filename":"scripts/package_skill.py","content":"#!/usr/bin/env python3\n\"\"\"\nSkill Packager - Create ZIP files for Claude.ai web/desktop upload.\n\nClaude Code (CLI) and Claude.ai (web/desktop) have SEPARATE skill systems.\nSkills in .claude/skills/ work in the terminal but do NOT appear in the\nClaude.ai web Habilidades (Skills) settings page.\n\nTo install a skill in Claude.ai web/desktop, you need to upload a ZIP file\nthrough Settings > Capabilities > Skills > Upload skill.\n\nThis script packages skills into the correct ZIP format.\n\nUsage:\n python package_skill.py --source \"C:\\\\path\\\\to\\\\skill\"\n python package_skill.py --source \"C:\\\\path\\\\to\\\\skill\" --output \"C:\\\\Users\\\\renat\\\\Desktop\"\n python package_skill.py --all\n python package_skill.py --all --output \"C:\\\\Users\\\\renat\\\\Desktop\"\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport re\nimport zipfile\nfrom pathlib import Path\n\n# ── Configuration ──────────────────────────────────────────────────────────\n\nSKILLS_ROOT = Path(r\"C:\\Users\\renat\\skills\")\nDEFAULT_OUTPUT = Path(os.path.expanduser(\"~\")) / \"Desktop\"\n\n# Directories to exclude from ZIP\nEXCLUDE_DIRS = {\n \".git\", \"__pycache__\", \"node_modules\", \".venv\", \"venv\",\n \".tox\", \".mypy_cache\", \".pytest_cache\", \"data\",\n}\n\n# File patterns to exclude\nEXCLUDE_FILES = {\n \".env\", \".gitignore\", \".DS_Store\", \"Thumbs.db\",\n \"credentials.json\", \"token.json\",\n}\n\nEXCLUDE_EXTENSIONS = {\n \".pyc\", \".pyo\", \".db\", \".sqlite\", \".sqlite3\",\n \".log\", \".tmp\", \".bak\",\n}\n\n\n# ── YAML Frontmatter Parser ───────────────────────────────────────────────\n\ndef parse_yaml_frontmatter(path: Path) -> dict:\n \"\"\"Extract YAML frontmatter from a SKILL.md file.\"\"\"\n try:\n text = path.read_text(encoding=\"utf-8\")\n except Exception:\n return {}\n\n match = re.match(r\"^---\\s*\\n(.*?)\\n---\", text, re.DOTALL)\n if not match:\n return {}\n\n try:\n import yaml\n return yaml.safe_load(match.group(1)) or {}\n except Exception:\n result = {}\n block = match.group(1)\n for key in (\"name\", \"description\", \"version\"):\n m = re.search(rf'^{key}:\\s*[\"\\']?(.+?)[\"\\']?\\s*

Skill Installer v3.0 Overview Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao. When to Use This Skill - When the user mentions "instalar skill" or related topics - When the user mentions "install skill" or related topics - When the user mentions "registrar skill" or related topics - When the user mentions "nova skill" or related topics - When the user mentions "new skill" or related topics - When the user mentions "adicionar skill ao ecossistema" or related topics Do Not Use This Skill When…

, block, re.MULTILINE)\n if m:\n result[key] = m.group(1).strip()\n else:\n m2 = re.search(\n rf'^{key}:\\s*>-?\\s*\\n((?:\\s+.+\\n?)+)', block, re.MULTILINE\n )\n if m2:\n lines = m2.group(1).strip().split(\"\\n\")\n result[key] = \" \".join(line.strip() for line in lines)\n return result\n\n\n# ── Validation ─────────────────────────────────────────────────────────────\n\ndef validate_for_web(skill_dir: Path) -> dict:\n \"\"\"Validate skill meets Claude.ai web upload requirements.\"\"\"\n errors = []\n warnings = []\n\n skill_md = skill_dir / \"SKILL.md\"\n if not skill_md.exists():\n errors.append(\"SKILL.md not found\")\n return {\"valid\": False, \"errors\": errors, \"warnings\": warnings}\n\n meta = parse_yaml_frontmatter(skill_md)\n\n # Name: required, lowercase, letters/numbers/hyphens only, max 64 chars\n name = meta.get(\"name\", \"\")\n if not name:\n errors.append(\"Missing 'name' field in frontmatter\")\n else:\n if name != name.lower():\n warnings.append(f\"Name '{name}' should be lowercase: '{name.lower()}'\")\n name_lower = name.lower()\n if len(name_lower) == 1:\n if not re.match(r'^[a-z0-9]

Skill Installer v3.0 Overview Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao. When to Use This Skill - When the user mentions "instalar skill" or related topics - When the user mentions "install skill" or related topics - When the user mentions "registrar skill" or related topics - When the user mentions "nova skill" or related topics - When the user mentions "new skill" or related topics - When the user mentions "adicionar skill ao ecossistema" or related topics Do Not Use This Skill When…

, name_lower):\n warnings.append(f\"Name '{name}' should only contain lowercase letters or numbers\")\n elif not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]

Skill Installer v3.0 Overview Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao. When to Use This Skill - When the user mentions "instalar skill" or related topics - When the user mentions "install skill" or related topics - When the user mentions "registrar skill" or related topics - When the user mentions "nova skill" or related topics - When the user mentions "new skill" or related topics - When the user mentions "adicionar skill ao ecossistema" or related topics Do Not Use This Skill When…

, name_lower):\n warnings.append(f\"Name '{name}' should only contain lowercase letters, numbers, and hyphens\")\n if len(name) > 64:\n errors.append(f\"Name exceeds 64 characters: {len(name)}\")\n if \"anthropic\" in name_lower or \"claude\" in name_lower:\n errors.append(f\"Name cannot contain reserved words 'anthropic' or 'claude'\")\n\n # Description: required, max 1024 chars\n desc = meta.get(\"description\", \"\")\n if not desc:\n errors.append(\"Missing 'description' field in frontmatter\")\n elif len(desc) > 1024:\n warnings.append(f\"Description exceeds 1024 chars ({len(desc)}), may be truncated\")\n\n return {\n \"valid\": len(errors) == 0,\n \"errors\": errors,\n \"warnings\": warnings,\n \"name\": name,\n \"description\": desc[:120] if desc else \"\",\n }\n\n\ndef should_include(file_path: Path, skill_dir: Path) -> bool:\n \"\"\"Check if a file should be included in the ZIP.\"\"\"\n rel = file_path.relative_to(skill_dir)\n\n # Check directory exclusions\n for part in rel.parts[:-1]:\n if part in EXCLUDE_DIRS:\n return False\n\n # Check file exclusions\n if file_path.name in EXCLUDE_FILES:\n return False\n\n # Check extension exclusions\n if file_path.suffix.lower() in EXCLUDE_EXTENSIONS:\n return False\n\n return True\n\n\n# ── Packaging ──────────────────────────────────────────────────────────────\n\ndef package_skill(skill_dir: Path, output_dir: Path = None) -> dict:\n \"\"\"\n Package a skill directory into a ZIP file for Claude.ai upload.\n\n The ZIP format required by Claude.ai:\n skill-name.zip\n └── skill-name/\n ├── SKILL.md\n ├── scripts/\n ├── references/\n └── ...\n \"\"\"\n skill_dir = Path(skill_dir).resolve()\n\n if not skill_dir.exists():\n return {\"success\": False, \"error\": f\"Directory not found: {skill_dir}\"}\n\n # Validate\n validation = validate_for_web(skill_dir)\n if not validation[\"valid\"]:\n return {\n \"success\": False,\n \"error\": f\"Validation failed: {'; '.join(validation['errors'])}\",\n \"validation\": validation,\n }\n\n skill_name = validation[\"name\"] or skill_dir.name\n skill_name_lower = skill_name.lower()\n\n # Determine output path\n if output_dir is None:\n output_dir = DEFAULT_OUTPUT\n output_dir = Path(output_dir).resolve()\n output_dir.mkdir(parents=True, exist_ok=True)\n\n zip_path = output_dir / f\"{skill_name_lower}.zip\"\n\n # Collect files\n files_to_include = []\n for root, dirs, files in os.walk(skill_dir):\n # Filter directories in-place to skip excluded ones\n dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]\n\n for f in files:\n fp = Path(root) / f\n if should_include(fp, skill_dir):\n files_to_include.append(fp)\n\n if not files_to_include:\n return {\"success\": False, \"error\": \"No files to package\"}\n\n # Create ZIP with skill folder as root\n # CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes\n try:\n with zipfile.ZipFile(zip_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for fp in sorted(files_to_include):\n rel_path = fp.relative_to(skill_dir)\n # Convert Windows backslash to forward slash for ZIP compatibility\n rel_posix = rel_path.as_posix()\n archive_path = f\"{skill_name_lower}/{rel_posix}\"\n zf.write(fp, archive_path)\n\n # Verify ZIP is not empty and valid\n with zipfile.ZipFile(zip_path, \"r\") as zf_check:\n entries = zf_check.namelist()\n if not entries:\n zip_path.unlink(missing_ok=True)\n return {\"success\": False, \"error\": \"ZIP was created empty (no files written)\"}\n # Verify no backslash paths leaked through\n bad_paths = [e for e in entries if \"\\\\\" in e]\n if bad_paths:\n zip_path.unlink(missing_ok=True)\n return {\n \"success\": False,\n \"error\": f\"ZIP contains backslash paths (invalid): {bad_paths[:3]}\",\n }\n\n # Get ZIP size\n zip_size = zip_path.stat().st_size\n zip_size_kb = zip_size / 1024\n\n except Exception as e:\n return {\"success\": False, \"error\": f\"ZIP creation failed: {e}\"}\n\n return {\n \"success\": True,\n \"zip_path\": str(zip_path),\n \"skill_name\": skill_name_lower,\n \"files_count\": len(files_to_include),\n \"zip_size_kb\": round(zip_size_kb, 1),\n \"validation\": validation,\n \"upload_instructions\": (\n \"Para instalar no Claude.ai web/desktop:\\n\"\n \"1. Abra claude.ai > Settings > Capabilities > Skills\\n\"\n \"2. Clique em 'Upload skill'\\n\"\n f\"3. Selecione o arquivo: {zip_path}\\n\"\n \"4. A skill aparecera na lista de Habilidades\"\n ),\n }\n\n\ndef package_all(output_dir: Path = None) -> dict:\n \"\"\"Package all installed skills.\"\"\"\n results = []\n\n # Find all skill directories with SKILL.md\n for item in sorted(SKILLS_ROOT.iterdir()):\n if not item.is_dir():\n continue\n if item.name.startswith(\".\"):\n continue\n if item.name == \"agent-orchestrator\":\n continue # Meta-skill, not for web upload\n\n skill_md = item / \"SKILL.md\"\n if skill_md.exists():\n result = package_skill(item, output_dir)\n results.append(result)\n\n # Also check nested skills\n for parent in SKILLS_ROOT.iterdir():\n if not parent.is_dir() or parent.name.startswith(\".\"):\n continue\n for child in parent.iterdir():\n if child.is_dir() and (child / \"SKILL.md\").exists():\n if child.name != \"agent-orchestrator\":\n result = package_skill(child, output_dir)\n results.append(result)\n\n success = sum(1 for r in results if r[\"success\"])\n failed = sum(1 for r in results if not r[\"success\"])\n\n return {\n \"total\": len(results),\n \"success\": success,\n \"failed\": failed,\n \"results\": results,\n }\n\n\n# ── Verify Existing ZIPs ──────────────────────────────────────────────────\n\ndef verify_zips(output_dir: Path = None) -> dict:\n \"\"\"Verify all existing skill ZIPs for integrity.\n\n Checks:\n - ZIP is valid and not corrupted\n - Contains SKILL.md\n - No backslash paths\n - Not empty\n - Matches a known skill name\n \"\"\"\n if output_dir is None:\n output_dir = DEFAULT_OUTPUT\n\n results = []\n zip_files = sorted(output_dir.glob(\"*.zip\"))\n\n for zip_path in zip_files:\n entry = {\n \"file\": str(zip_path),\n \"name\": zip_path.stem,\n \"size_kb\": round(zip_path.stat().st_size / 1024, 1),\n \"valid\": False,\n \"issues\": [],\n }\n\n try:\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n entries = zf.namelist()\n\n if not entries:\n entry[\"issues\"].append(\"ZIP is empty\")\n results.append(entry)\n continue\n\n # Check for backslash paths\n bad_paths = [e for e in entries if \"\\\\\" in e]\n if bad_paths:\n entry[\"issues\"].append(f\"Contains backslash paths: {bad_paths[:3]}\")\n\n # Check for SKILL.md\n has_skill_md = any(e.endswith(\"SKILL.md\") for e in entries)\n if not has_skill_md:\n entry[\"issues\"].append(\"Missing SKILL.md\")\n\n # Test integrity\n bad_file = zf.testzip()\n if bad_file:\n entry[\"issues\"].append(f\"Corrupted file in ZIP: {bad_file}\")\n\n entry[\"file_count\"] = len(entries)\n entry[\"valid\"] = len(entry[\"issues\"]) == 0\n\n except zipfile.BadZipFile:\n entry[\"issues\"].append(\"Not a valid ZIP file\")\n except Exception as e:\n entry[\"issues\"].append(f\"Error reading ZIP: {e}\")\n\n results.append(entry)\n\n valid = sum(1 for r in results if r[\"valid\"])\n invalid = sum(1 for r in results if not r[\"valid\"])\n\n return {\n \"total\": len(results),\n \"valid\": valid,\n \"invalid\": invalid,\n \"output_dir\": str(output_dir),\n \"results\": results,\n }\n\n\n# ── CLI Entry Point ───────────────────────────────────────────────────────\n\ndef main():\n args = sys.argv[1:]\n\n source = None\n output_dir = None\n do_all = \"--all\" in args\n do_verify = \"--verify\" in args\n\n if \"--source\" in args:\n idx = args.index(\"--source\")\n if idx + 1 \u003c len(args):\n source = args[idx + 1]\n\n if \"--output\" in args:\n idx = args.index(\"--output\")\n if idx + 1 \u003c len(args):\n output_dir = Path(args[idx + 1])\n\n if do_verify:\n result = verify_zips(Path(output_dir) if output_dir else None)\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0 if result[\"invalid\"] == 0 else 1)\n\n if not source and not do_all:\n print(json.dumps({\n \"error\": \"Usage: python package_skill.py \u003ccommand>\",\n \"commands\": {\n \"--source \u003cpath>\": \"Package a single skill\",\n \"--source \u003cpath> --output \u003cdir>\": \"Package to specific directory\",\n \"--all\": \"Package all installed skills\",\n \"--all --output \u003cdir>\": \"Package all to specific directory\",\n \"--verify\": \"Verify integrity of all existing ZIPs\",\n \"--verify --output \u003cdir>\": \"Verify ZIPs in specific directory\",\n },\n }, indent=2))\n sys.exit(1)\n\n if source:\n result = package_skill(Path(source), output_dir)\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0 if result[\"success\"] else 1)\n elif do_all:\n result = package_all(output_dir)\n print(json.dumps(result, indent=2, ensure_ascii=False))\n sys.exit(0 if result[\"failed\"] == 0 else 1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14454,"content_sha256":"0a74e18df005fd6a9119f39c014f04792263ea2d2c7314da3a8aee9aba06a3b9"},{"filename":"scripts/requirements.txt","content":"pyyaml>=6.0\n","content_type":"text/plain; charset=utf-8","language":null,"size":12,"content_sha256":"8cfc3197b86bf23f2454918d3a0e212585c9cc70f8eee9ee36518311a93c7eb9"},{"filename":"scripts/validate_skill.py","content":"#!/usr/bin/env python3\n\"\"\"\nSkill Validator - Deep validation of a skill directory.\n\nPerforms 10 checks on a skill directory to ensure it's properly structured\nand ready for installation.\n\nUsage:\n python validate_skill.py \"C:\\\\path\\\\to\\\\skill\"\n python validate_skill.py \"C:\\\\path\\\\to\\\\skill\" --strict\n python validate_skill.py \"C:\\\\path\\\\to\\\\skill\" --registry \"C:\\\\path\\\\to\\\\registry.json\"\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport re\nfrom pathlib import Path\n\n# ── Constants ──────────────────────────────────────────────────────────────\n\nFORBIDDEN_PATTERNS = [\n \".env\",\n \"credentials.json\",\n \"credentials.yaml\",\n \"credentials.yml\",\n \"*.key\",\n \"*.pem\",\n \"*.p12\",\n \"*.pfx\",\n \".secrets\",\n \"secret.json\",\n \"token.json\",\n \".aws/credentials\",\n]\n\nMAX_SIZE_MB = 50\nMIN_DESCRIPTION_LENGTH = 50\n\nSKILLS_ROOT = Path(r\"C:\\Users\\renat\\skills\")\nREGISTRY_PATH = SKILLS_ROOT / \"agent-orchestrator\" / \"data\" / \"registry.json\"\n\n\n# ── YAML Frontmatter Parser ───────────────────────────────────────────────\n\ndef parse_yaml_frontmatter(path: Path) -> dict:\n \"\"\"Extract YAML frontmatter from a SKILL.md file.\n\n Mirrors the parser from scan_registry.py for consistency.\n \"\"\"\n try:\n text = path.read_text(encoding=\"utf-8\")\n except Exception:\n return {}\n\n match = re.match(r\"^---\\s*\\n(.*?)\\n---\", text, re.DOTALL)\n if not match:\n return {}\n\n try:\n import yaml\n return yaml.safe_load(match.group(1)) or {}\n except Exception:\n # Fallback: manual parsing\n result = {}\n block = match.group(1)\n for key in (\"name\", \"description\", \"version\", \"capabilities\"):\n m = re.search(rf'^{key}:\\s*[\"\\']?(.+?)[\"\\']?\\s*

Skill Installer v3.0 Overview Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao. When to Use This Skill - When the user mentions "instalar skill" or related topics - When the user mentions "install skill" or related topics - When the user mentions "registrar skill" or related topics - When the user mentions "nova skill" or related topics - When the user mentions "new skill" or related topics - When the user mentions "adicionar skill ao ecossistema" or related topics Do Not Use This Skill When…

, block, re.MULTILINE)\n if m:\n result[key] = m.group(1).strip()\n else:\n m2 = re.search(\n rf'^{key}:\\s*>-?\\s*\\n((?:\\s+.+\\n?)+)', block, re.MULTILINE\n )\n if m2:\n lines = m2.group(1).strip().split(\"\\n\")\n result[key] = \" \".join(line.strip() for line in lines)\n return result\n\n\n# ── Validation Checks ─────────────────────────────────────────────────────\n\ndef check_skill_md_exists(skill_dir: Path) -> dict:\n \"\"\"Check 1: SKILL.md exists.\"\"\"\n skill_md = skill_dir / \"SKILL.md\"\n exists = skill_md.exists() and skill_md.is_file()\n return {\n \"check\": 1,\n \"name\": \"SKILL.md exists\",\n \"status\": \"pass\" if exists else \"fail\",\n \"message\": str(skill_md) if exists else f\"SKILL.md not found in {skill_dir}\",\n }\n\n\ndef check_frontmatter_parseable(skill_dir: Path) -> dict:\n \"\"\"Check 2: YAML frontmatter is present and parseable.\"\"\"\n skill_md = skill_dir / \"SKILL.md\"\n if not skill_md.exists():\n return {\n \"check\": 2,\n \"name\": \"Frontmatter parseable\",\n \"status\": \"fail\",\n \"message\": \"SKILL.md does not exist\",\n }\n\n try:\n text = skill_md.read_text(encoding=\"utf-8\")\n except Exception as e:\n return {\n \"check\": 2,\n \"name\": \"Frontmatter parseable\",\n \"status\": \"fail\",\n \"message\": f\"Cannot read SKILL.md: {e}\",\n }\n\n match = re.match(r\"^---\\s*\\n(.*?)\\n---\", text, re.DOTALL)\n if not match:\n return {\n \"check\": 2,\n \"name\": \"Frontmatter parseable\",\n \"status\": \"fail\",\n \"message\": \"No YAML frontmatter found (expected --- delimiters)\",\n }\n\n meta = parse_yaml_frontmatter(skill_md)\n if not meta:\n return {\n \"check\": 2,\n \"name\": \"Frontmatter parseable\",\n \"status\": \"fail\",\n \"message\": \"Frontmatter found but could not be parsed\",\n }\n\n return {\n \"check\": 2,\n \"name\": \"Frontmatter parseable\",\n \"status\": \"pass\",\n \"message\": f\"Parsed fields: {', '.join(meta.keys())}\",\n }\n\n\ndef check_name_exists(meta: dict) -> dict:\n \"\"\"Check 3: 'name' field exists and is non-empty.\"\"\"\n name = meta.get(\"name\", \"\")\n has_name = bool(name and str(name).strip())\n return {\n \"check\": 3,\n \"name\": \"Field 'name' present\",\n \"status\": \"pass\" if has_name else \"fail\",\n \"message\": f\"name: {name}\" if has_name else \"Missing or empty 'name' field\",\n }\n\n\ndef check_description_exists(meta: dict) -> dict:\n \"\"\"Check 4: 'description' field exists and is non-empty.\"\"\"\n desc = meta.get(\"description\", \"\")\n has_desc = bool(desc and str(desc).strip())\n return {\n \"check\": 4,\n \"name\": \"Field 'description' present\",\n \"status\": \"pass\" if has_desc else \"fail\",\n \"message\": (\n f\"description: {str(desc)[:80]}...\"\n if has_desc\n else \"Missing or empty 'description' field\"\n ),\n }\n\n\ndef check_description_length(meta: dict) -> dict:\n \"\"\"Check 5: Description has >= 50 characters (warning if shorter).\"\"\"\n desc = str(meta.get(\"description\", \"\"))\n length = len(desc)\n ok = length >= MIN_DESCRIPTION_LENGTH\n return {\n \"check\": 5,\n \"name\": \"Description length >= 50 chars\",\n \"status\": \"pass\" if ok else \"warn\",\n \"message\": (\n f\"Length: {length} chars\"\n if ok\n else f\"Description only {length} chars (recommend >= {MIN_DESCRIPTION_LENGTH})\"\n ),\n }\n\n\ndef check_name_matches_dir(skill_dir: Path, meta: dict) -> dict:\n \"\"\"Check 6: 'name' matches directory name (warning if mismatch).\"\"\"\n name = str(meta.get(\"name\", \"\")).strip().lower()\n dir_name = skill_dir.name.lower()\n\n if not name:\n return {\n \"check\": 6,\n \"name\": \"Name matches directory\",\n \"status\": \"warn\",\n \"message\": \"No name field to compare\",\n }\n\n matches = name == dir_name\n return {\n \"check\": 6,\n \"name\": \"Name matches directory\",\n \"status\": \"pass\" if matches else \"warn\",\n \"message\": (\n f\"'{name}' == '{dir_name}'\"\n if matches\n else f\"Name '{name}' differs from directory '{dir_name}'\"\n ),\n }\n\n\ndef check_forbidden_files(skill_dir: Path) -> dict:\n \"\"\"Check 7: No forbidden files (.env, credentials, keys, etc.).\"\"\"\n found_forbidden = []\n\n for root, _dirs, files in os.walk(skill_dir):\n for f in files:\n f_lower = f.lower()\n for pattern in FORBIDDEN_PATTERNS:\n if pattern.startswith(\"*.\"):\n ext = pattern[1:] # e.g., \".key\"\n if f_lower.endswith(ext):\n found_forbidden.append(os.path.join(root, f))\n break\n else:\n if f_lower == pattern.lower():\n found_forbidden.append(os.path.join(root, f))\n break\n\n if found_forbidden:\n return {\n \"check\": 7,\n \"name\": \"No forbidden files\",\n \"status\": \"fail\",\n \"message\": f\"Found {len(found_forbidden)} forbidden file(s): {', '.join(found_forbidden[:5])}\",\n }\n\n return {\n \"check\": 7,\n \"name\": \"No forbidden files\",\n \"status\": \"pass\",\n \"message\": \"No forbidden files detected\",\n }\n\n\ndef check_total_size(skill_dir: Path) -> dict:\n \"\"\"Check 8: Total size is reasonable (warn if > 50MB).\"\"\"\n total = 0\n for root, _dirs, files in os.walk(skill_dir):\n for f in files:\n try:\n total += os.path.getsize(os.path.join(root, f))\n except OSError:\n pass\n\n size_mb = total / (1024 * 1024)\n ok = size_mb \u003c= MAX_SIZE_MB\n\n return {\n \"check\": 8,\n \"name\": f\"Size \u003c= {MAX_SIZE_MB}MB\",\n \"status\": \"pass\" if ok else \"warn\",\n \"message\": f\"Total: {size_mb:.1f} MB\" + (\"\" if ok else f\" (exceeds {MAX_SIZE_MB}MB)\"),\n }\n\n\ndef check_scripts_requirements(skill_dir: Path) -> dict:\n \"\"\"Check 9: If scripts/ exists, check for requirements.txt.\"\"\"\n scripts_dir = skill_dir / \"scripts\"\n if not scripts_dir.exists():\n return {\n \"check\": 9,\n \"name\": \"scripts/ has requirements.txt\",\n \"status\": \"skip\",\n \"message\": \"No scripts/ directory (check not applicable)\",\n }\n\n has_reqs = (scripts_dir / \"requirements.txt\").exists()\n return {\n \"check\": 9,\n \"name\": \"scripts/ has requirements.txt\",\n \"status\": \"pass\" if has_reqs else \"warn\",\n \"message\": (\n \"requirements.txt found\"\n if has_reqs\n else \"scripts/ exists but no requirements.txt\"\n ),\n }\n\n\ndef check_duplicate_name(meta: dict, registry_path: Path) -> dict:\n \"\"\"Check 10: Name not duplicated in existing registry.\"\"\"\n name = str(meta.get(\"name\", \"\")).strip().lower()\n if not name:\n return {\n \"check\": 10,\n \"name\": \"No duplicate in registry\",\n \"status\": \"warn\",\n \"message\": \"No name to check\",\n }\n\n if not registry_path.exists():\n return {\n \"check\": 10,\n \"name\": \"No duplicate in registry\",\n \"status\": \"pass\",\n \"message\": \"No registry.json found (skip check)\",\n }\n\n try:\n registry = json.loads(registry_path.read_text(encoding=\"utf-8\"))\n existing_names = [\n s.get(\"name\", \"\").lower() for s in registry.get(\"skills\", [])\n ]\n if name in existing_names:\n return {\n \"check\": 10,\n \"name\": \"No duplicate in registry\",\n \"status\": \"warn\",\n \"message\": f\"Skill '{name}' already exists in registry (use --force to overwrite)\",\n }\n except Exception as e:\n return {\n \"check\": 10,\n \"name\": \"No duplicate in registry\",\n \"status\": \"warn\",\n \"message\": f\"Could not read registry: {e}\",\n }\n\n return {\n \"check\": 10,\n \"name\": \"No duplicate in registry\",\n \"status\": \"pass\",\n \"message\": f\"Name '{name}' not found in registry\",\n }\n\n\n# ── Main Validation ───────────────────────────────────────────────────────\n\ndef validate(skill_dir: Path, strict: bool = False, registry_path: Path = None) -> dict:\n \"\"\"Run all 10 validation checks on a skill directory.\n\n Returns:\n dict with keys: valid (bool), checks (list), warnings (list), errors (list)\n \"\"\"\n if registry_path is None:\n registry_path = REGISTRY_PATH\n\n skill_dir = Path(skill_dir).resolve()\n\n if not skill_dir.exists():\n return {\n \"valid\": False,\n \"skill_dir\": str(skill_dir),\n \"checks\": [],\n \"warnings\": [],\n \"errors\": [f\"Directory does not exist: {skill_dir}\"],\n }\n\n # Parse frontmatter once\n skill_md = skill_dir / \"SKILL.md\"\n meta = parse_yaml_frontmatter(skill_md) if skill_md.exists() else {}\n\n # Run all 10 checks\n checks = [\n check_skill_md_exists(skill_dir), # 1\n check_frontmatter_parseable(skill_dir), # 2\n check_name_exists(meta), # 3\n check_description_exists(meta), # 4\n check_description_length(meta), # 5\n check_name_matches_dir(skill_dir, meta), # 6\n check_forbidden_files(skill_dir), # 7\n check_total_size(skill_dir), # 8\n check_scripts_requirements(skill_dir), # 9\n check_duplicate_name(meta, registry_path), # 10\n ]\n\n errors = [c for c in checks if c[\"status\"] == \"fail\"]\n warnings = [c for c in checks if c[\"status\"] == \"warn\"]\n passed = [c for c in checks if c[\"status\"] in (\"pass\", \"skip\")]\n\n # In strict mode, warnings are treated as errors\n if strict:\n errors.extend(warnings)\n warnings = []\n\n valid = len(errors) == 0\n\n return {\n \"valid\": valid,\n \"skill_dir\": str(skill_dir),\n \"skill_name\": meta.get(\"name\", skill_dir.name),\n \"total_checks\": len(checks),\n \"passed\": len(passed),\n \"warnings_count\": len(warnings),\n \"errors_count\": len(errors),\n \"checks\": checks,\n \"warnings\": [f\"Check {w['check']}: {w['message']}\" for w in warnings],\n \"errors\": [f\"Check {e['check']}: {e['message']}\" for e in errors],\n }\n\n\n# ── CLI Entry Point ───────────────────────────────────────────────────────\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(json.dumps({\n \"valid\": False,\n \"error\": \"Usage: python validate_skill.py \u003cskill-directory> [--strict] [--registry \u003cpath>]\",\n }, indent=2))\n sys.exit(1)\n\n skill_dir = Path(sys.argv[1]).resolve()\n strict = \"--strict\" in sys.argv\n registry_path = None\n\n if \"--registry\" in sys.argv:\n idx = sys.argv.index(\"--registry\")\n if idx + 1 \u003c len(sys.argv):\n registry_path = Path(sys.argv[idx + 1])\n\n result = validate(skill_dir, strict=strict, registry_path=registry_path)\n print(json.dumps(result, indent=2, ensure_ascii=False))\n\n sys.exit(0 if result[\"valid\"] else 1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13754,"content_sha256":"aa50dbdec21e052d9fca9482bbc9c3a4f167f5f967896f2aed2e34d8052058da"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Skill Installer v3.0","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use This Skill","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the user mentions \"instalar skill\" or related topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the user mentions \"install skill\" or related topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the user mentions \"registrar skill\" or related topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the user mentions \"nova skill\" or related topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the user mentions \"new skill\" or related topics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the user mentions \"adicionar skill ao ecossistema\" or related topics","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Do Not Use This Skill When","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The task is unrelated to skill installer","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A simpler, more specific tool can handle the request","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The user needs general-purpose assistance without domain expertise","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How It Works","type":"text"}]},{"type":"paragraph","content":[{"text":"Agente instalador enterprise-grade que garante que toda skill criada (via skill-creator ou manualmente) seja corretamente instalada, registrada e verificada no ecossistema. Inclui auto-repair, rollback, dry-run, dashboard, e diagnostico avancado.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Principio: Redundancia Maxima","type":"text"}]},{"type":"paragraph","content":[{"text":"Seis camadas de validacao garantem que nenhuma skill fique mal-instalada:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Camada","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"O que valida","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"detect_skills.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SKILL.md existe + tem frontmatter","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validate_skill.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10 checks profundos","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"install_skill.py (pre)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Conflitos, permissoes, espaco, versao","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"install_skill.py (pos)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Arquivos copiados corretamente","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scan_registry.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skill aparece no registry (com deduplicacao)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"package_skill.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ZIP valido sem backslashes, nao-vazio, integrity check","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Localizacao","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"C:\\Users\\renat\\skills\\skill-installer\\\n├── SKILL.md \u003c- este arquivo\n├── scripts/\n│ ├── install_skill.py \u003c- instalador principal (11 passos) + todos os comandos\n│ ├── detect_skills.py \u003c- scanner de skills nao-instaladas\n│ ├── validate_skill.py \u003c- validacao profunda (10 checks)\n│ ├── package_skill.py \u003c- empacotador ZIP + verificador de integridade\n│ └── requirements.txt\n├── references/\n│ └── known-locations.md\n└── data/\n ├── install_log.json \u003c- log de operacoes (auto-gerado, com rotacao)\n ├── backups/ \u003c- backups antes de sobrescrever\n └── staging/ \u003c- area temporaria para copias seguras","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow Principal","type":"text"}]},{"type":"paragraph","content":[{"text":"Quando esta skill for ativada, siga estes passos na ordem:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 1: Apos Skill-Creator Finalizar","type":"text"}]},{"type":"paragraph","content":[{"text":"O skill-creator acabou de criar uma skill em algum diretorio. Execute:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --source \"\u003ccaminho-da-skill-criada>\" --force","type":"text"}]},{"type":"paragraph","content":[{"text":"Substitua ","type":"text"},{"text":"\u003ccaminho-da-skill-criada>","type":"text","marks":[{"type":"code_inline"}]},{"text":" pelo diretorio onde o skill-creator salvou a skill.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 2: Usuario Pede Para Instalar Uma Skill Especifica","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --source \"\u003ccaminho>\" [--name \"nome-override\"] [--force]","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 3: Simular Instalacao Sem Fazer Nada (Dry-Run)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --source \"\u003ccaminho>\" --dry-run","type":"text"}]},{"type":"paragraph","content":[{"text":"Mostra exatamente o que seria feito em cada um dos 11 passos, sem alterar nenhum arquivo.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 4: Detectar E Instalar Skills Pendentes","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --detect\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --detect --auto","type":"text"}]},{"type":"paragraph","content":[{"text":"Escaneia locais conhecidos (Desktop, Downloads, Temp, workspaces) e apresenta candidatos com timestamps e tamanho. Com --auto instala todos automaticamente.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 5: Desinstalar Uma Skill","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --uninstall \"nome-da-skill\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Remove de ","type":"text"},{"text":"skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".claude/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", atualiza o registry e remove ZIP do Desktop. Backup automatico e feito antes da remocao.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 6: Health Check + Auto-Repair","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --health\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --health --repair","type":"text"}]},{"type":"paragraph","content":[{"text":"--health","type":"text","marks":[{"type":"code_inline"}]},{"text":" verifica TODAS as skills: frontmatter, registro, registry, duplicatas. ","type":"text"},{"text":"--health --repair","type":"text","marks":[{"type":"code_inline"}]},{"text":" encontra problemas E os corrige automaticamente:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skills nao registradas -> registra","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skills faltando no registry -> atualiza","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Duplicatas -> remove","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 7: Rollback (Restaurar De Backup)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --rollback \"nome-da-skill\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Encontra o backup mais recente da skill e restaura para o estado anterior. Re-registra e atualiza o registry automaticamente.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 8: Reinstalar Todas As Skills","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --reinstall-all","type":"text"}]},{"type":"paragraph","content":[{"text":"Re-registra TODAS as skills em ","type":"text"},{"text":".claude/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", re-empacota todos os ZIPs, e atualiza o registry. Util apos mudancas em massa ou migracao.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 9: Dashboard De Status","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --status","type":"text"}]},{"type":"paragraph","content":[{"text":"Exibe dashboard rico com: nome, versao, saude, registro, backups de cada skill, estatisticas de operacoes (installs, uninstalls, rollbacks).","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cenario 10: Ver Historico De Operacoes","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --log\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\install_skill.py --log 50","type":"text"}]},{"type":"paragraph","content":[{"text":"Mostra as ultimas N operacoes com timestamp, tipo, skill e resultado.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Validar Uma Skill","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\validate_skill.py \"C:\\caminho\\para\\skill\"\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\validate_skill.py \"C:\\caminho\\para\\skill\" --strict","type":"text"}]},{"type":"paragraph","content":[{"text":"Retorna JSON com ","type":"text"},{"text":"valid","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bool), ","type":"text"},{"text":"checks","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"warnings","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"errors","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Detectar Skills Nao-Instaladas","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\detect_skills.py\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\detect_skills.py --path \"C:\\diretorio\\especifico\"\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\detect_skills.py --all","type":"text"}]},{"type":"paragraph","content":[{"text":"Retorna JSON com candidatos incluindo: ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"source_path","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"already_installed","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"valid_frontmatter","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"last_modified","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"size_kb","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"file_count","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Empacotar Zip Para Claude.Ai","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\package_skill.py --source \"C:\\caminho\"\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\package_skill.py --all\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\package_skill.py --all --output \"C:\\Users\\renat\\Desktop\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Verificar Integridade De Zips Existentes","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python C:\\Users\\renat\\skills\\skill-installer\\scripts\\package_skill.py --verify\npython C:\\Users\\renat\\skills\\skill-installer\\scripts\\package_skill.py --verify --output \"C:\\Users\\renat\\Desktop\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Install_Skill.Py","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Comando","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Descricao","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--source \u003cpath>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Instalar skill de caminho","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--source \u003cpath> --force","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sobrescrever se existir","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--source \u003cpath> --name \u003cnome>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Nome customizado","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--source \u003cpath> --dry-run","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Simular sem alterar","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--detect","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-detectar skills pendentes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--detect --auto","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Detectar e instalar automaticamente","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--uninstall \u003cnome>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Desinstalar (com backup)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--rollback \u003cnome>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Restaurar do ultimo backup","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--reinstall-all","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Re-registrar + re-empacotar todas","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--health","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Health check de todas as skills","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--health --repair","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Health check + auto-correcao","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dashboard rico com versoes, saude, backups","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--log [N]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ultimas N operacoes (padrao: 20)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Saida JSON em vez de texto formatado","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"O Que O Instalador Faz (11 Passos)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolver fonte","type":"text","marks":[{"type":"strong"}]},{"text":" - identifica o diretorio da skill","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validar","type":"text","marks":[{"type":"strong"}]},{"text":" - roda 10 checks no SKILL.md e estrutura","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Determinar nome","type":"text","marks":[{"type":"strong"}]},{"text":" - extrai do frontmatter ou usa --name, compara versoes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verificar conflitos","type":"text","marks":[{"type":"strong"}]},{"text":" - checa se ja existe no destino","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backup","type":"text","marks":[{"type":"strong"}]},{"text":" - se sobrescrevendo, faz backup timestamped (exclui backups/ e staging/)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Copiar via staging","type":"text","marks":[{"type":"strong"}]},{"text":" - copia para area temp, valida hash, depois move","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Registrar no Claude Code CLI","type":"text","marks":[{"type":"strong"}]},{"text":" - copia SKILL.md para .claude/skills/\u003cnome>/","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Atualizar registry","type":"text","marks":[{"type":"strong"}]},{"text":" - roda scan_registry.py --force (com deduplicacao por nome)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verificar instalacao","type":"text","marks":[{"type":"strong"}]},{"text":" - confirma arquivos, registry, registro (5 checks)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Empacotar ZIP","type":"text","marks":[{"type":"strong"}]},{"text":" - cria ZIP para upload no Claude.ai web/desktop (validado)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Logar operacao","type":"text","marks":[{"type":"strong"}]},{"text":" - append em install_log.json (com rotacao automatica)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"IMPORTANTE","type":"text","marks":[{"type":"strong"}]},{"text":": Skills no Claude Code (CLI) e Claude.ai (web/desktop) sao SEPARADAS. O instalador cobre ambas superficies automaticamente.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Seguranca","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backups automaticos","type":"text","marks":[{"type":"strong"}]},{"text":": antes de qualquer sobrescrita, backup em ","type":"text"},{"text":"data/backups/\u003cnome>_\u003ctimestamp>/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Staging area","type":"text","marks":[{"type":"strong"}]},{"text":": copia para temp primeiro, valida hash, depois move (minimiza corrupcao)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Idempotencia","type":"text","marks":[{"type":"strong"}]},{"text":": rodar 2x com mesma source detecta hashes identicos, nao duplica","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Arquivos proibidos","type":"text","marks":[{"type":"strong"}]},{"text":": bloqueia instalacao se encontrar .env, *.key, ","type":"text"},{"text":".pem, credentials.","type":"text","marks":[{"type":"em"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Log com rotacao","type":"text","marks":[{"type":"strong"}]},{"text":": toda operacao logada; mantem ultimas 500 entradas","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Limite de backups","type":"text","marks":[{"type":"strong"}]},{"text":": mantem ultimos 5 por skill, limpa automaticamente","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Anti-recursao","type":"text","marks":[{"type":"strong"}]},{"text":": backup e staging excluem seus proprios subdiretorios","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deduplicacao no registry","type":"text","marks":[{"type":"strong"}]},{"text":": scan_registry.py deduplica por nome (case-insensitive)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ZIP validado","type":"text","marks":[{"type":"strong"}]},{"text":": verifica ausencia de backslashes, conteudo nao-vazio, integridade","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dry-run","type":"text","marks":[{"type":"strong"}]},{"text":": simula instalacao completa sem tocar nenhum arquivo","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rollback","type":"text","marks":[{"type":"strong"}]},{"text":": restaura de backup com re-registro automatico","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Comparacao de versao","type":"text","marks":[{"type":"strong"}]},{"text":": detecta upgrade/downgrade/same antes de sobrescrever","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hash normalizado","type":"text","marks":[{"type":"strong"}]},{"text":": md5_dir usa forward slashes e exclui dirs de sistema","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Integracao Com Orchestrator","type":"text"}]},{"type":"paragraph","content":[{"text":"Esta skill e auto-detectada pelo ","type":"text"},{"text":"scan_registry.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" e matchada pelo ","type":"text"},{"text":"match_skills.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" quando o usuario menciona keywords de instalacao. Nenhuma configuracao manual necessaria.","type":"text"}]},{"type":"paragraph","content":[{"text":"Alem disso, o CLAUDE.md global contem instrucao para rodar o instalador automaticamente apos o skill-creator finalizar uma skill.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Best Practices","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provide clear, specific context about your project and requirements","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Review all suggestions before applying them to production code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Combine with other complementary skills for comprehensive analysis","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Pitfalls","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Using this skill for tasks outside its domain expertise","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Applying recommendations without understanding your specific context","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not providing enough project context for accurate analysis","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"skill-sentinel","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Complementary skill for enhanced analysis","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Limitations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use this skill only when the task clearly matches the scope described above.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not treat the output as a substitute for environment-specific validation, testing, or expert review.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"skill-installer","risk":"safe","tags":["skill-management","deployment","validation","installation"],"tools":["claude-code","antigravity","cursor","gemini-cli","codex-cli"],"author":"@skillopedia","source":{"stars":39376,"repo_name":"antigravity-awesome-skills","origin_url":"https://github.com/sickn33/antigravity-awesome-skills/blob/HEAD/skills/skill-installer/SKILL.md","repo_owner":"sickn33","body_sha256":"beec0c4abf55feee34f37b2749ba322db5d67d1e8b34f85f3c8353d7f8b22516","cluster_key":"662a01c9bab73dc8014cbde6a73fc4483c0dd93b14b9829da6af380dda3bdae9","clean_bundle":{"format":"clean-skill-bundle-v1","source":"sickn33/antigravity-awesome-skills/skills/skill-installer/SKILL.md","attachments":[{"id":"79f5900d-dae7-5267-ae8d-246bb486fd26","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/79f5900d-dae7-5267-ae8d-246bb486fd26/attachment.md","path":"references/known-locations.md","size":1337,"sha256":"468161b054ed2d37b12b9940a2003e46665f6b0b3464b4cabbb41d6163476fc8","contentType":"text/markdown; charset=utf-8"},{"id":"c4876a5a-c436-5608-ba8a-e6d5a8708e39","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c4876a5a-c436-5608-ba8a-e6d5a8708e39/attachment.py","path":"scripts/detect_skills.py","size":10601,"sha256":"a67840d2a279fab253fd408397a4e9573a5172bc0768c444c3e86add25deda81","contentType":"text/x-python; charset=utf-8"},{"id":"0124382f-e78e-5e88-9c32-b767086c6433","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0124382f-e78e-5e88-9c32-b767086c6433/attachment.py","path":"scripts/install_skill.py","size":62199,"sha256":"cb94865e9b2ecf10d4e18a583bbed9cb811acd5b001d9b81be4c3af6738a7c93","contentType":"text/x-python; charset=utf-8"},{"id":"3ac07244-59ef-5c26-9517-a8d82102692c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3ac07244-59ef-5c26-9517-a8d82102692c/attachment.py","path":"scripts/package_skill.py","size":14454,"sha256":"0a74e18df005fd6a9119f39c014f04792263ea2d2c7314da3a8aee9aba06a3b9","contentType":"text/x-python; charset=utf-8"},{"id":"accdd369-20df-565f-8247-a1b27293ae56","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/accdd369-20df-565f-8247-a1b27293ae56/attachment.txt","path":"scripts/requirements.txt","size":12,"sha256":"8cfc3197b86bf23f2454918d3a0e212585c9cc70f8eee9ee36518311a93c7eb9","contentType":"text/plain; charset=utf-8"},{"id":"e9b0bbf4-d816-5c7a-b248-754b180edf5b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e9b0bbf4-d816-5c7a-b248-754b180edf5b/attachment.py","path":"scripts/validate_skill.py","size":13754,"sha256":"aa50dbdec21e052d9fca9482bbc9c3a4f167f5f967896f2aed2e34d8052058da","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"54c13a60b68e9cbe135e2134882f0d2edb11c3737dd55c7391562574a50effb8","attachment_count":6,"text_attachments":6,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":3,"skill_md_path":"skills/skill-installer/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":2},"version":"v1","category":"devops-infrastructure","date_added":"2026-03-06","import_tag":"clean-skills-v1","description":"Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao."}},"renderedAt":1782982097829}

Skill Installer v3.0 Overview Instala, valida, registra e verifica novas skills no ecossistema. 10 checks de seguranca, copia, registro no orchestrator e verificacao pos-instalacao. When to Use This Skill - When the user mentions "instalar skill" or related topics - When the user mentions "install skill" or related topics - When the user mentions "registrar skill" or related topics - When the user mentions "nova skill" or related topics - When the user mentions "new skill" or related topics - When the user mentions "adicionar skill ao ecossistema" or related topics Do Not Use This Skill When…