#!/usr/bin/env bash # pdlc-skills installer for Claude Code. # # Thin wrapper around `claude plugin marketplace add` + `claude plugin install`. # Lets users install/upgrade/uninstall pdlc-skills with a single curl # one-liner; falls back to the local clone as a marketplace source when run # from a checkout (so contributors can test their changes locally). set -euo pipefail GITHUB_OWNER="kanfu-panda" GITHUB_REPO="pdlc-skills" PLUGIN_NAME="pdlc" MARKETPLACE_NAME="pdlc-skills" # matches `name` in .claude-plugin/marketplace.json GITHUB_RAW_VERSION_URL="https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/main/VERSION" # ─── Detect mode ─── SCRIPT_DIR="" if [[ -n "${BASH_SOURCE[0]:-}" ]]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || SCRIPT_DIR="" fi IS_LOCAL_CLONE=0 if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/.claude-plugin/plugin.json" ]]; then IS_LOCAL_CLONE=1 fi # Marketplace source: prefer local clone (for contributors), else GitHub repo. if [[ "$IS_LOCAL_CLONE" -eq 1 ]]; then MARKETPLACE_SOURCE="$SCRIPT_DIR" else MARKETPLACE_SOURCE="${GITHUB_OWNER}/${GITHUB_REPO}" fi # ─── Sanity: claude CLI present ─── require_claude_cli() { if ! command -v claude >/dev/null 2>&1; then cat >&2 <<'EOF' Error: `claude` CLI not found in PATH. pdlc-skills is a Claude Code plugin and requires the Claude Code CLI for install. Get Claude Code: https://docs.anthropic.com/claude-code EOF exit 1 fi } usage() { cat </.agents/skills/ are read by GitHub Copilot, Gemini CLI, OpenCode, Amp and Goose; use --dest for tools with their own directory (Cursor, Windsurf, Kiro, Roo Code). Only pdlc-* directories are ever written or removed. Remote (no clone) install one-liner: curl -fsSL https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/main/install.sh | bash -s -- --global Equivalent native commands: claude plugin marketplace add ${GITHUB_OWNER}/${GITHUB_REPO} claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME} EOF } # ver_lt A B —— A 是否严格小于 B(按 x.y.z 逐段数值比较;1.7.9 < 1.7.10) ver_lt() { local a b i x y IFS=. read -r -a a <<< "$1" IFS=. read -r -a b <<< "$2" for i in 0 1 2; do x="${a[$i]:-0}"; y="${b[$i]:-0}" x="${x%%[!0-9]*}"; y="${y%%[!0-9]*}" (( 10#${x:-0} < 10#${y:-0} )) && return 0 (( 10#${x:-0} > 10#${y:-0} )) && return 1 done return 1 } # ─── Subcommand: --version ─── do_version() { local installed_ver="not installed" if command -v claude >/dev/null 2>&1; then installed_ver=$( claude plugin list 2>/dev/null \ | awk '/'"${PLUGIN_NAME}@${MARKETPLACE_NAME}"'/{found=1; next} found && /Version:/{print $2; exit}' ) [[ -z "$installed_ver" ]] && installed_ver="not installed" fi local latest_ver="" if command -v curl >/dev/null 2>&1; then latest_ver=$(curl -fsSL --max-time 5 "$GITHUB_RAW_VERSION_URL" 2>/dev/null | head -1 || true) fi : "${latest_ver:=unable to fetch}" echo "pdlc-skills version status" echo "──────────────────────────────────────" if [[ "$IS_LOCAL_CLONE" -eq 1 ]]; then echo " Local clone: $(head -1 "$SCRIPT_DIR/VERSION" 2>/dev/null || echo unknown)" fi echo " Installed: ${installed_ver}" echo " Latest: ${latest_ver}" # 其它工具用的投影由 install.sh --target codex | agents 装,装时写了版本戳 local f for f in "${HOME}/.codex/skills/.pdlc-version:Codex skills" "${HOME}/.agents/skills/.pdlc-version:Agent Skills"; do [[ -f "${f%%:*}" ]] && echo " ${f#*:}: $(head -1 "${f%%:*}")" done echo "" if [[ "$latest_ver" != "unable to fetch" \ && "$installed_ver" != "not installed" ]] && ver_lt "$installed_ver" "$latest_ver"; then echo "⚠️ Installed (${installed_ver}) is behind latest (${latest_ver})." echo " Upgrade: bash install.sh --upgrade" elif [[ "$installed_ver" == "not installed" ]]; then echo "ℹ️ Not installed yet." echo " Install: bash install.sh --global" elif [[ "$latest_ver" == "unable to fetch" ]]; then # 查不了就说查不了,不能报「已是最新」 echo "ℹ️ Couldn't fetch the latest version (offline?), so whether ${installed_ver} is current is unknown." else echo "✅ Up to date." fi } # ─── Target: codex (multi-platform adapter, see docs/decisions/0003) ─── # Target Codex is a Claude-Code-compatible distribution: it reads native skills at # ~/.codex/skills//SKILL.md (description-triggered, NOT slash commands). # Verified on real Codex (gpt-5.6-sol matched pdlc-prd by description and ran it). CODEX_SKILLS_DIR="${HOME}/.codex/skills" CODEX_PROMPTS_DIR="${HOME}/.codex/prompts" # legacy (v1.5.0 wrongly installed here) — cleaned on install/uninstall CODEX_PDLC_DIR="${HOME}/.codex/pdlc" # install_projection <构建产物的 skills 目录> <目标目录> # 先拷进目标目录下的暂存目录、核对数量,成功后才删旧换新——拷到一半失败(磁盘满、权限)时用户原有的安装还在。 # 只动 pdlc-* 与版本戳,目标目录里的其它 skill 不碰。 install_projection() { local src="$1" dest="$2" stage want got d stage="${dest}/.pdlc-staging.$$" rm -rf "$stage" mkdir -p "$stage" if ! cp -R "$src"/pdlc-* "$stage"/; then rm -rf "$stage"; echo "Error: copy to ${dest} failed; the existing install was left untouched." >&2; exit 1 fi want=$(find "$src" -mindepth 1 -maxdepth 1 -type d -name 'pdlc-*' | wc -l | tr -d ' ') got=$(find "$stage" -mindepth 1 -maxdepth 1 -type d -name 'pdlc-*' | wc -l | tr -d ' ') if [[ "$want" -eq 0 || "$want" != "$got" ]]; then rm -rf "$stage"; echo "Error: staged ${got} of ${want} skills; the existing install was left untouched." >&2; exit 1 fi rm -rf "${dest:?}"/pdlc-*/ for d in "$stage"/pdlc-*; do mv "$d" "$dest"/; done rmdir "$stage" if [[ "$IS_LOCAL_CLONE" -eq 1 ]]; then head -1 "$SCRIPT_DIR/VERSION" > "$dest/.pdlc-version"; fi } require_python3() { if ! command -v python3 >/dev/null 2>&1; then echo "Error: python3 not found — required to build the Codex adapter." >&2 exit 1 fi } # Remove the v1.5.0 mistaken prompts install so upgraders don't keep dead files. clean_legacy_codex_prompts() { if compgen -G "${CODEX_PROMPTS_DIR}/pdlc-*.md" >/dev/null 2>&1; then echo "Cleaning legacy v1.5.0 prompts from ${CODEX_PROMPTS_DIR} ..." rm -f "${CODEX_PROMPTS_DIR}"/pdlc-*.md fi } do_codex_install() { if [[ "$IS_LOCAL_CLONE" -ne 1 ]]; then cat >&2 <" echo " Methodology: ${CODEX_PDLC_DIR}" echo " Note: the statusline and the autonomous loop engine are Claude Code-only (not ported)." echo "" echo " ⚠️ Scope: this adapter targets Codex distributions that read ~/.codex/skills/" echo " (Claude-Code-compatible ones). Vanilla OpenAI Codex is UNVERIFIED — we have" echo " no such environment to test on. If Codex ignores the skills after a restart," echo " it does not read that directory; fall back to the platform-neutral route:" echo " append ${CODEX_PDLC_DIR}/pdlc-methodology.md to your project's AGENTS.md" echo " and drive PDLC in natural language. Reports welcome via GitHub issues." } do_codex_uninstall() { # Only ever touches our own namespaced paths under ~/.codex. echo "Removing pdlc skills from ${CODEX_SKILLS_DIR} ..." rm -rf "${CODEX_SKILLS_DIR}"/pdlc-*/ "${CODEX_SKILLS_DIR}/.pdlc-version" clean_legacy_codex_prompts if [[ "$CODEX_PDLC_DIR" == "${HOME}/.codex/pdlc" && -d "$CODEX_PDLC_DIR" ]]; then rm -rf "$CODEX_PDLC_DIR" fi echo "✅ Removed pdlc from Codex. Restart Codex to drop the skills." } # ─── Target: agents (Agent Skills open standard, see docs/decisions/0007) ─── # One standard projection for every tool that loads Agent Skills. Destination: # --dest DIR → DIR # --project DIR → DIR/.agents/skills # (default) → ~/.agents/skills agents_dest() { if [[ -n "$DEST" ]]; then echo "$DEST" elif [[ "$SCOPE" == "project" ]]; then echo "$PROJECT/.agents/skills" else echo "${HOME}/.agents/skills" fi } do_agents_install() { if [[ "$IS_LOCAL_CLONE" -ne 1 ]]; then cat >&2 <" echo " Verified tools and the ones still unverified are listed in the README (\"Supported platforms\")." } do_agents_uninstall() { local dest dest="$(agents_dest)" echo "Removing pdlc skills from ${dest} ..." rm -rf "${dest:?}"/pdlc-*/ "${dest:?}/.pdlc-version" echo "✅ Removed. Other skills in ${dest} were not touched." } # ─── Argument parsing ─── ACTION="install" SCOPE="" PROJECT="" TARGET="claude" # claude (default) | codex | agents DEST="" # 带值的选项:缺值时 set -e 下的 shift 2 会让脚本不声不响地退出,所以先点名报错。 # 下一个参数以 - 开头也算缺值,免得 --dest --uninstall 把选项当成目录 need_value() { if [[ -z "${2:-}" || "${2}" == -* ]]; then echo "Error: $1 requires a value" >&2 usage >&2 exit 1 fi } while [[ $# -gt 0 ]]; do case "$1" in --global) SCOPE="user"; shift ;; --project) need_value "$1" "${2:-}"; SCOPE="project"; PROJECT="$2"; shift 2 ;; --target) need_value "$1" "${2:-}"; TARGET="$2"; shift 2 ;; --dest) need_value "$1" "${2:-}"; DEST="$2"; shift 2 ;; --uninstall) ACTION="uninstall"; shift ;; --upgrade) ACTION="upgrade"; shift ;; --version) ACTION="version"; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown argument: $1" >&2; usage; exit 1 ;; esac done case "$ACTION" in version) do_version; exit 0 ;; esac # Codex target bypasses the claude CLI (it copies files, not a plugin install). if [[ "$TARGET" == "codex" ]]; then case "$ACTION" in install) do_codex_install; exit 0 ;; uninstall) do_codex_uninstall; exit 0 ;; *) echo "Error: --target codex supports install / --uninstall only." >&2; exit 1 ;; esac elif [[ "$TARGET" == "agents" ]]; then if [[ "$SCOPE" == "project" && -z "$DEST" ]]; then [[ -n "$PROJECT" ]] || { echo "Error: --project requires a path." >&2; exit 1; } [[ -d "$PROJECT" ]] || { echo "Error: project directory not found: $PROJECT" >&2; exit 1; } fi case "$ACTION" in install) do_agents_install; exit 0 ;; uninstall) do_agents_uninstall; exit 0 ;; *) echo "Error: --target agents supports install / --uninstall only." >&2; exit 1 ;; esac elif [[ "$TARGET" != "claude" ]]; then echo "Error: unknown --target '$TARGET' (supported: claude, codex, agents)." >&2 exit 1 fi [[ -z "$DEST" ]] || { echo "Error: --dest is only valid with --target agents." >&2; exit 1; } require_claude_cli # Interactive scope selection (skipped in non-TTY) if [[ -z "$SCOPE" && "$ACTION" == "install" ]]; then if [[ ! -t 0 ]]; then echo "Error: --global or --project required when piped via curl." >&2 usage exit 1 fi echo "Choose install scope:" echo " 1) Global (for current user — ~/.claude/plugins/)" echo " 2) Project (scoped to one repo — /.claude/plugins/)" read -rp "Enter 1 or 2: " choice case "$choice" in 1) SCOPE="user" ;; 2) SCOPE="project" read -rp "Project root path: " PROJECT ;; *) echo "Invalid choice"; exit 1 ;; esac fi # Validate project path early if [[ "$SCOPE" == "project" ]]; then [[ -n "$PROJECT" ]] || { echo "Error: --project requires a path." >&2; exit 1; } [[ -d "$PROJECT" ]] || { echo "Error: project directory not found: $PROJECT" >&2; exit 1; } fi case "$ACTION" in install) # Add marketplace (idempotent — claude CLI handles already-added gracefully) echo "Adding marketplace: ${MARKETPLACE_SOURCE}" claude plugin marketplace add "$MARKETPLACE_SOURCE" 2>&1 | tail -3 || true echo "" echo "Installing plugin: ${PLUGIN_NAME}@${MARKETPLACE_NAME} (scope: ${SCOPE:-user})" if [[ "$SCOPE" == "project" ]]; then (cd "$PROJECT" && claude plugin install "${PLUGIN_NAME}@${MARKETPLACE_NAME}" --scope project) else claude plugin install "${PLUGIN_NAME}@${MARKETPLACE_NAME}" --scope user fi echo "" echo "✅ Done." echo "" echo "Restart Claude Code, then type /pdlc- to see all 38 sub-commands." echo "" echo "⭐ If pdlc helps you ship, a star helps others find it:" echo " https://github.com/kanfu-panda/pdlc-skills" ;; upgrade) # 先刷新 marketplace:否则 plugin update 只看本地缓存的旧目录,可能「升级」到原来的版本 echo "Refreshing marketplace ${MARKETPLACE_NAME}..." claude plugin marketplace update "${MARKETPLACE_NAME}" 2>&1 | tail -3 echo "Updating ${PLUGIN_NAME}@${MARKETPLACE_NAME}..." claude plugin update "${PLUGIN_NAME}@${MARKETPLACE_NAME}" echo "" echo "✅ Updated. Restart Claude Code to apply." echo "" echo "⭐ If pdlc helps you ship, a star helps others find it:" echo " https://github.com/kanfu-panda/pdlc-skills" ;; uninstall) echo "Uninstalling ${PLUGIN_NAME}@${MARKETPLACE_NAME}..." claude plugin uninstall "${PLUGIN_NAME}@${MARKETPLACE_NAME}" echo "" echo "✅ Uninstalled." ;; esac