#!/usr/bin/env bash

set -eo pipefail

REPO_ROOT="${WARP_COMMON_SKILLS_TARGET_REPO_ROOT:-$(pwd)}"
LOCK_FILE=""
STAMP_RELATIVE_PATH="warp/common-skills-lock.hash"
SKILLS_CLI_VERSION="1.5.6"
COMMON_SKILLS_SOURCE="warpdotdev/common-skills"
COMMON_SKILLS_REF="${WARP_COMMON_SKILLS_REF:-}"
COMMON_SKILLS_REQUEST_SOURCE="${COMMON_SKILLS_SOURCE}"
SKILLS_CLI_AGENT="warp"
COMMON_SKILL_SELECTOR="*"

IF_NEEDED=0
FORCE=0
QUIET=0
PROMPT_FOR_TARGET=0
NON_INTERACTIVE=0
VERIFY_ONLY=0
TARGET="${WARP_COMMON_SKILLS_INSTALL_TARGET:-}"
RESOLVED_TARGET=""
ALLOW_GLOBAL_LOCK_UPDATE=0
UPDATED_LOCK_CANDIDATE=""

if [[ -n "${COMMON_SKILLS_REF}" ]]; then
  COMMON_SKILLS_REQUEST_SOURCE="${COMMON_SKILLS_SOURCE}#${COMMON_SKILLS_REF}"
fi

resolve_repo_root() {
  cd "$1" && pwd
}

lock_hash() {
  git -C "${REPO_ROOT}" hash-object "${LOCK_FILE}"
}

project_stamp_file() {
  local git_path=""

  if git_path="$(git -C "${REPO_ROOT}" rev-parse --git-path "${STAMP_RELATIVE_PATH}" 2>/dev/null)"; then
    case "${git_path}" in
      /*)
        echo "${git_path}"
        ;;
      *)
        echo "${REPO_ROOT}/${git_path}"
        ;;
    esac
    return
  fi

  echo "${REPO_ROOT}/.agents/skills/.common-skills-lock.hash"
}

project_exclude_file() {
  local git_path=""

  if git_path="$(git -C "${REPO_ROOT}" rev-parse --git-path "info/exclude" 2>/dev/null)"; then
    case "${git_path}" in
      /*)
        echo "${git_path}"
        ;;
      *)
        echo "${REPO_ROOT}/${git_path}"
        ;;
    esac
    return
  fi

  return 1
}

global_stamp_file() {
  echo "${HOME}/.agents/warp/common-skills-lock.hash"
}

stamp_file() {
  if [[ "${TARGET}" = "global" ]]; then
    global_stamp_file
  else
    project_stamp_file
  fi
}

stamp_hash() {
  local file="$1"

  if [[ -f "${file}" ]]; then
    cat "${file}"
  fi
}

normalize_common_skills_target() {
  local target
  target="$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]')"

  case "${target}" in
    ""|p|project|1)
      echo "project"
      ;;
    g|global|2)
      echo "global"
      ;;
    *)
      return 1
      ;;
  esac
}

locked_skill_names() {
  node -e '
const fs = require("fs");
const lock = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
for (const name of Object.keys(lock.skills || {})) {
  console.log(name);
}
' "${LOCK_FILE}"
}

locked_skill_paths() {
  node -e '
const fs = require("fs");
const lock = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
for (const skill of Object.values(lock.skills || {})) {
  console.log(skill.skillPath);
}
' "${LOCK_FILE}"
}

locked_common_skills_source() {
  node -e '
const fs = require("fs");
const lock = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
const skill = Object.values(lock.skills || {})[0];
if (!skill) {
  process.exit(1);
}
const source = skill.ref ? `${skill.source}#${skill.ref}` : skill.source;
console.log(source);
' "${LOCK_FILE}"
}

target_description() {
  if [[ "${TARGET}" = "global" ]]; then
    echo "~/.agents/skills"
  else
    echo ".agents/skills"
  fi
}

validate_required_commands() {
  local missing=0
  local tool=""

  for tool in "$@"; do
    if ! command -v "${tool}" >/dev/null 2>&1; then
      echo "error: required command '${tool}' is not installed or not on PATH." >&2
      missing=1
    fi
  done

  if [[ "${missing}" -eq 1 ]]; then
    echo "Install the missing tools, then rerun scripts/install_common_skills." >&2
    exit 1
  fi
}

validate_verify_dependencies() {
  validate_required_commands git node
}

validate_install_dependencies() {
  validate_required_commands git node npm npx
}

usage() {
  cat <<EOF
Usage: scripts/install_common_skills [options]

Install or update common agent skills from the standard project skills-lock.json.
If skills-lock.json is missing, create it from warpdotdev/common-skills first.

Options:
  --repo-root <path>   Repository containing skills-lock.json and project skills.
  --project            Install into this checkout's .agents/skills directory.
  --global             Install into ~/.agents/skills for the current user.
  --if-needed          Skip installation when the local stamp matches skills-lock.json.
  --prompt-for-target  Prompt for project/global before installing or updating when no target is explicit.
  --non-interactive    Do not prompt; fail when no install target is explicit.
  --force              Install even if the local stamp is already up to date.
  --quiet              Suppress "already up to date" informational output.
  --verify-only        Verify common skills without installing or updating them.
  -h, --help           Show this help message.

Environment:
  WARP_SKIP_COMMON_SKILLS_INSTALL=1
      Skip installing common agent skills.
  WARP_COMMON_SKILLS_INSTALL_TARGET=project|global
      Choose the install target when no explicit --project/--global flag is provided.
      If omitted in an interactive invocation, prompt with global as the
      recommended default. If omitted in a non-interactive invocation, fail.
  WARP_COMMON_SKILLS_TARGET_REPO_ROOT=/path/to/repo
      Repository containing skills-lock.json and project-local .agents/skills.
  WARP_COMMON_SKILLS_REF=<git-ref>
      Use a branch, tag, or commit from warpdotdev/common-skills when creating
      a missing lock or checking interactively for upstream lock updates.
EOF
}

common_skills_present_in_project() {
  local path=""

  while IFS= read -r path; do
    if [[ -f "${REPO_ROOT}/${path}" ]]; then
      return 0
    fi
  done < <(common_skill_paths)

  return 1
}

common_skills_present_in_global() {
  local name=""
  if [[ -f "$(global_stamp_file)" ]]; then
    return 0
  fi

  while IFS= read -r name; do
    if [[ -f "${HOME}/.agents/skills/${name}/SKILL.md" ]]; then
      return 0
    fi
  done < <(common_skill_names)

  return 1
}

prompt_common_skills_target() {
  local default_target="${1:-global}"
  local reply=""
  local target=""
  if [[ "${NON_INTERACTIVE}" -eq 1 ]]; then
    echo "error: common skills install target is required in non-interactive mode; pass --project, --global, or set WARP_COMMON_SKILLS_INSTALL_TARGET=project|global." >&2
    return 1
  elif can_prompt_interactively; then
    echo "Where should common agent skills be installed?" > /dev/tty
    if [[ "${default_target}" = "global" ]]; then
      echo "  1) Project checkout .agents/skills" > /dev/tty
      echo "  2) Global user skills ~/.agents/skills (recommended for local installs, default)" > /dev/tty
      printf "Install common skills to [project/global] (default: global): " > /dev/tty
    else
      echo "  1) Project checkout .agents/skills (default)" > /dev/tty
      echo "  2) Global user skills ~/.agents/skills" > /dev/tty
      printf "Install common skills to [project/global] (default: project): " > /dev/tty
    fi
    read -r reply < /dev/tty
    if [[ -z "${reply}" ]]; then
      echo "${default_target}"
      return
    fi
    if target="$(normalize_common_skills_target "${reply}")"; then
      echo "${target}"
    else
      echo "error: unrecognized install target '${reply}'." >&2
      return 1
    fi
  else
    echo "error: common skills install target is required when no interactive prompt is available; pass --project, --global, or set WARP_COMMON_SKILLS_INSTALL_TARGET=project|global." >&2
    return 1
  fi
}

resolve_common_skills_target() {
  local project_present=0
  local global_present=0
  local target=""

  if common_skills_present_in_project; then
    project_present=1
  fi

  if common_skills_present_in_global; then
    global_present=1
  fi

  if [[ "${project_present}" -eq 1 && "${global_present}" -eq 1 ]]; then
    echo "Common agent skills are installed in both this checkout's .agents/skills and ~/.agents/skills." >&2
    echo "Remove one copy with scripts/remove_common_skills --repo-root ${REPO_ROOT} or scripts/remove_common_skills --repo-root ${REPO_ROOT} --global, then rerun." >&2
    return 1
  fi

  if [[ -n "${TARGET}" ]]; then
    if ! target="$(normalize_common_skills_target "${TARGET}")"; then
      return 1
    fi

    if [[ "${target}" = "project" && "${global_present}" -eq 1 ]]; then
      echo "Common agent skills are already installed globally in ~/.agents/skills." >&2
      echo "Remove the global copy before installing them in this checkout's .agents/skills." >&2
      return 1
    fi

    if [[ "${target}" = "global" && "${project_present}" -eq 1 ]]; then
      echo "Common agent skills are already installed in this checkout's .agents/skills." >&2
      echo "Remove the project copy before installing them globally in ~/.agents/skills." >&2
      return 1
    fi

    echo "${target}"
    return
  fi
  if [[ "${PROMPT_FOR_TARGET}" -eq 1 ]]; then
    target="$(prompt_common_skills_target "global")"

    if [[ "${target}" = "project" && "${global_present}" -eq 1 ]]; then
      echo "Common agent skills are already installed globally in ~/.agents/skills." >&2
      echo "Remove the global copy before installing them in this checkout's .agents/skills." >&2
      return 1
    fi

    if [[ "${target}" = "global" && "${project_present}" -eq 1 ]]; then
      echo "Common agent skills are already installed in this checkout's .agents/skills." >&2
      echo "Remove the project copy before installing them globally in ~/.agents/skills." >&2
      return 1
    fi

    echo "${target}"
    return
  fi
  if [[ "${NON_INTERACTIVE}" -eq 1 ]]; then
    echo "error: common skills install target is required in non-interactive mode; pass --project, --global, or set WARP_COMMON_SKILLS_INSTALL_TARGET=project|global." >&2
    return 1
  fi

  prompt_common_skills_target "global"
}

log() {
  if [[ "${QUIET}" -ne 1 ]]; then
    echo "$@"
  fi
}

can_prompt_interactively() {
  [[ "${NON_INTERACTIVE}" -ne 1 ]] && { true < /dev/tty; } 2>/dev/null && { true > /dev/tty; } 2>/dev/null
}

fail_if_target_requires_unavailable_prompt() {
  if [[ -n "${TARGET}" || "${VERIFY_ONLY}" -eq 1 ]]; then
    return 0
  fi

  if [[ "${NON_INTERACTIVE}" -eq 1 ]]; then
    echo "error: common skills install target is required in non-interactive mode; pass --project, --global, or set WARP_COMMON_SKILLS_INSTALL_TARGET=project|global." >&2
    exit 1
  fi

  if ! can_prompt_interactively; then
    echo "error: common skills install target is required when no interactive prompt is available; pass --project, --global, or set WARP_COMMON_SKILLS_INSTALL_TARGET=project|global." >&2
    exit 1
  fi
}

prepare_updated_lock_candidate() {
  local temp_dir=""
  local update_output=""

  temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/common-skills-lock.XXXXXX")"
  update_output="${temp_dir}/update.log"

  cp "${LOCK_FILE}" "${temp_dir}/skills-lock.json"
  git -C "${temp_dir}" init >/dev/null 2>&1 || true

  if ! (
    cd "${temp_dir}"
    npx --yes "skills@${SKILLS_CLI_VERSION}" add "${COMMON_SKILLS_REQUEST_SOURCE}" -a "${SKILLS_CLI_AGENT}" -s "${COMMON_SKILL_SELECTOR}" -y --copy
  ) < /dev/null > "${update_output}" 2>&1; then
    if [[ "${QUIET}" -ne 1 ]]; then
      echo "warning: could not check for updated common skills from ${COMMON_SKILLS_REQUEST_SOURCE}; continuing with the existing skills-lock.json." >&2
    fi
    rm -rf "${temp_dir}"
    return 1
  fi

  if [[ ! -f "${temp_dir}/skills-lock.json" ]]; then
    if [[ "${QUIET}" -ne 1 ]]; then
      echo "warning: common skills update check did not produce a candidate skills-lock.json; continuing with the existing lock." >&2
    fi
    rm -rf "${temp_dir}"
    return 1
  fi

  if cmp -s "${LOCK_FILE}" "${temp_dir}/skills-lock.json"; then
    rm -rf "${temp_dir}"
    return 1
  fi

  UPDATED_LOCK_CANDIDATE="$(mktemp "${TMPDIR:-/tmp}/common-skills-lock-candidate.XXXXXX")"
  cp "${temp_dir}/skills-lock.json" "${UPDATED_LOCK_CANDIDATE}"
  rm -rf "${temp_dir}"
  return 0
}

maybe_prompt_for_upstream_lock_update() {
  local reply=""

  if [[ ! -f "${LOCK_FILE}" ]]; then
    return 0
  fi

  if ! can_prompt_interactively; then
    return 0
  fi

  if ! prepare_updated_lock_candidate; then
    return 0
  fi

  echo "Common agent skills have been updated in ${COMMON_SKILLS_REQUEST_SOURCE}." > /dev/tty
  printf "Update ${LOCK_FILE} and reinstall common skills from the updated lock? [y/N]: " > /dev/tty
  read -r reply < /dev/tty

  case "$(printf "%s" "${reply}" | tr '[:upper:]' '[:lower:]')" in
    y|yes)
      cp "${UPDATED_LOCK_CANDIDATE}" "${LOCK_FILE}"
      rm -f "${UPDATED_LOCK_CANDIDATE}"
      UPDATED_LOCK_CANDIDATE=""
      ALLOW_GLOBAL_LOCK_UPDATE=1
      FORCE=1
      echo "Updated skills-lock.json. Common skills will be reinstalled from the updated lock." > /dev/tty
      ;;
    *)
      rm -f "${UPDATED_LOCK_CANDIDATE}"
      UPDATED_LOCK_CANDIDATE=""
      echo "Keeping existing skills-lock.json. Common skills will be installed from the existing lock." > /dev/tty
      ;;
  esac
}

common_skill_names() {
  if [[ -f "${LOCK_FILE}" ]]; then
    locked_skill_names
  fi
}

common_skill_paths() {
  if [[ -f "${LOCK_FILE}" ]]; then
    locked_skill_paths
  fi
}

locked_skills_installed() {
  local name=""
  local path=""

  if [[ "${TARGET}" = "global" ]]; then
    while IFS= read -r name; do
      if [[ ! -f "${HOME}/.agents/skills/${name}/SKILL.md" ]]; then
        return 1
      fi
    done < <(locked_skill_names)
  else
    while IFS= read -r path; do
      if [[ ! -f "${REPO_ROOT}/${path}" ]]; then
        return 1
      fi
    done < <(locked_skill_paths)
  fi
}

common_skills_already_up_to_date_without_target() {
  local current_lock_hash=""
  local project_stamp_hash=""
  local global_stamp_hash=""

  if [[ ! -f "${LOCK_FILE}" ]]; then
    return 1
  fi

  fail_if_global_lock_mismatch

  current_lock_hash="$(lock_hash)"
  project_stamp_hash="$(stamp_hash "$(project_stamp_file)")"
  global_stamp_hash="$(stamp_hash "$(global_stamp_file)")"
  if [[ "${project_stamp_hash}" != "${current_lock_hash}" && "${global_stamp_hash}" != "${current_lock_hash}" ]]; then
    return 1
  fi

  verify_common_skills >/dev/null 2>&1
}

install_project_from_source() {
  (
    cd "${REPO_ROOT}"
    npx --yes "skills@${SKILLS_CLI_VERSION}" add "${COMMON_SKILLS_REQUEST_SOURCE}" -a "${SKILLS_CLI_AGENT}" -s "${COMMON_SKILL_SELECTOR}" -y --copy
  ) < /dev/null
}

install_project_from_lock() {
  (
    cd "${REPO_ROOT}"
    npx --yes "skills@${SKILLS_CLI_VERSION}" experimental_install
  ) < /dev/null
}

install_global_from_lock() {
  local skill_names=()
  local name=""
  local temp_dir=""
  local skill_dir=""

  while IFS= read -r name; do
    skill_names+=("${name}")
  done < <(locked_skill_names)

  if [[ "${#skill_names[@]}" -eq 0 ]]; then
    echo "error: no skills found in ${LOCK_FILE}" >&2
    exit 1
  fi

  temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/common-skills-global-install.XXXXXX")"
  cp "${LOCK_FILE}" "${temp_dir}/skills-lock.json"

  (
    cd "${temp_dir}"
    npx --yes "skills@${SKILLS_CLI_VERSION}" experimental_install
  ) < /dev/null

  mkdir -p "${HOME}/.agents/skills"
  for name in "${skill_names[@]}"; do
    skill_dir="${temp_dir}/.agents/skills/${name}"
    if [[ ! -d "${skill_dir}" ]]; then
      rm -rf "${temp_dir}"
      echo "error: expected ${name} to be restored from ${LOCK_FILE}" >&2
      exit 1
    fi

    rm -rf -- "${HOME}/.agents/skills/${name}"
    cp -R "${skill_dir}" "${HOME}/.agents/skills/${name}"
  done

  rm -rf "${temp_dir}"
}

remove_project_skills_from_lock() {
  local path=""
  local skill_dir=""

  while IFS= read -r path; do
    if [[ -z "${path}" ]]; then
      continue
    fi

    case "${path}" in
      .agents/skills/*/SKILL.md)
        skill_dir="$(dirname "${REPO_ROOT}/${path}")"
        ;;
      *)
        echo "error: refusing to remove unexpected skill path from lock file: ${path}" >&2
        exit 1
        ;;
    esac

    case "${skill_dir}" in
      "${REPO_ROOT}/.agents/skills/"*)
        rm -rf -- "${skill_dir}"
        ;;
      *)
        echo "error: refusing to remove path outside .agents/skills: ${skill_dir}" >&2
        exit 1
        ;;
    esac
  done < <(locked_skill_paths)
}

ensure_project_common_skills_ignored() {
  local exclude_file=""
  local path=""
  local skill_dir=""
  local tmp_file=""
  local marker_start="# BEGIN warp common-skills"
  local marker_end="# END warp common-skills"

  if [[ "${TARGET}" != "project" ]]; then
    return 0
  fi

  if ! exclude_file="$(project_exclude_file)"; then
    return 0
  fi

  mkdir -p "$(dirname "${exclude_file}")"
  touch "${exclude_file}"
  tmp_file="$(mktemp)"

  awk -v start="${marker_start}" -v end="${marker_end}" '
    $0 == start { skipping = 1; next }
    $0 == end { skipping = 0; next }
    !skipping { print }
  ' "${exclude_file}" > "${tmp_file}"

  {
    cat "${tmp_file}"
    echo "${marker_start}"
    while IFS= read -r path; do
      if [[ -z "${path}" ]]; then
        continue
      fi

      case "${path}" in
        .agents/skills/*/SKILL.md)
          skill_dir="${path%/SKILL.md}/"
          echo "/${skill_dir}"
          ;;
      esac
    done < <(locked_skill_paths)
    echo "${marker_end}"
  } > "${exclude_file}"

  rm -f "${tmp_file}"
}

install_from_lock() {
  if [[ "${TARGET}" = "global" ]]; then
    install_global_from_lock
  else
    install_project_from_lock
  fi
}

fail_if_global_lock_mismatch() {
  local target_stamp_file=""
  local target_stamp_hash=""
  local current_lock_hash=""

  if [[ "${TARGET}" != "global" ]]; then
    if [[ -n "${TARGET}" ]] || [[ ! -f "$(global_stamp_file)" ]]; then
      return 0
    fi
  fi

  target_stamp_file="$(global_stamp_file)"
  target_stamp_hash="$(stamp_hash "${target_stamp_file}")"
  if [[ -z "${target_stamp_hash}" ]]; then
    return 0
  fi

  current_lock_hash="$(lock_hash)"
  if [[ "${target_stamp_hash}" = "${current_lock_hash}" ]]; then
    return 0
  fi

  if [[ "${TARGET}" = "global" && "${ALLOW_GLOBAL_LOCK_UPDATE}" -eq 1 ]]; then
    return 0
  fi

  echo "error: global common skills are pinned to a different skills-lock.json than ${LOCK_FILE}." >&2
  echo "Another checkout may depend on the currently installed global common-skills version." >&2
  echo "To reuse the installed global copy, update this checkout to the same skills-lock.json and rerun." >&2
  echo "To keep this checkout's existing lock, rerun and choose project instead of global." >&2
  echo "To replace the global copy, run this from the consuming repo and then rerun:" >&2
  echo "  ./script/resolve_common_skills remove_common_skills -- --repo-root ${REPO_ROOT} --global" >&2
  exit 1
}

verify_common_skills() {
  if [[ ! -f "${LOCK_FILE}" ]]; then
    echo "error: missing skills lock file: ${LOCK_FILE}" >&2
    exit 1
  fi

  node - "${LOCK_FILE}" "${REPO_ROOT}" "${HOME}" "${TARGET}" "${QUIET}" "$(global_stamp_file)" <<'NODE'
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");

const lockFile = process.argv[2];
const repoRoot = process.argv[3];
const home = process.argv[4];
let target = process.argv[5];
const quiet = process.argv[6] === "1";
const globalStampFile = process.argv[7];

function fail(messages) {
  for (const message of messages) {
    console.error(message);
  }
  process.exit(1);
}

function lockedSkills() {
  const lock = JSON.parse(fs.readFileSync(lockFile, "utf8"));
  const skills = Object.entries(lock.skills || {});

  if (skills.length === 0) {
    fail([`error: no skills found in ${lockFile}`]);
  }

  return skills;
}

function projectSkillFile(skill) {
  return path.join(repoRoot, skill.skillPath);
}

function globalSkillFile(name) {
  return path.join(home, ".agents", "skills", name, "SKILL.md");
}

function targetSkillDir(name, skill, installTarget) {
  if (installTarget === "global") {
    return path.dirname(globalSkillFile(name));
  }

  return path.dirname(projectSkillFile(skill));
}

function containsLockedSkill(skills, installTarget) {
  return skills.some(([name, skill]) => {
    const skillFile = installTarget === "global"
      ? globalSkillFile(name)
      : projectSkillFile(skill);
    return fs.existsSync(skillFile);
  });
}

function collectFiles(baseDir, currentDir, results) {
  for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
    const fullPath = path.join(currentDir, entry.name);

    if (entry.isDirectory()) {
      if (entry.name === ".git" || entry.name === "node_modules") {
        continue;
      }

      collectFiles(baseDir, fullPath, results);
    } else if (entry.isFile()) {
      results.push({
        relativePath: path.relative(baseDir, fullPath).split(path.sep).join("/"),
        content: fs.readFileSync(fullPath),
      });
    }
  }
}

function normalizeCrlf(content) {
  // Git or platform tooling may rewrite checked-out text files as CRLF even
  // though the lockfile hash was computed from LF-normalized repository data.
  const carriageReturnByte = 13;
  const lineFeedByte = 10;
  const normalized = [];
  for (let index = 0; index < content.length; index++) {
    const byte = content[index];
    if (byte === carriageReturnByte && content[index + 1] === lineFeedByte) {
      continue;
    }
    normalized.push(byte);
  }
  return Buffer.from(normalized);
}

function computeSkillFolderHash(skillDir, normalizeLineEndings = false) {
  const files = [];
  collectFiles(skillDir, skillDir, files);
  files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));

  const hash = crypto.createHash("sha256");
  for (const file of files) {
    hash.update(file.relativePath);
    hash.update(normalizeLineEndings ? normalizeCrlf(file.content) : file.content);
  }

  return hash.digest("hex");
}

function verifyTarget(skills, installTarget) {
  const errors = [];

  for (const [name, skill] of skills) {
    const skillDir = targetSkillDir(name, skill, installTarget);
    const skillFile = path.join(skillDir, "SKILL.md");

    if (!fs.existsSync(skillFile)) {
      errors.push(`error: missing ${name} in ${installTarget === "global" ? "~/.agents/skills" : ".agents/skills"}`);
      continue;
    }

    const computedHash = computeSkillFolderHash(skillDir);
    // Preserve exact-byte verification first, then allow an equivalent CRLF
    // checkout to match the lockfile before reporting drift.
    const normalizedComputedHash =
      computedHash === skill.computedHash ? computedHash : computeSkillFolderHash(skillDir, true);
    if (normalizedComputedHash !== skill.computedHash) {
      errors.push(`error: ${name} does not match skills-lock.json`);
      errors.push(`  expected: ${skill.computedHash}`);
      errors.push(`  actual:   ${computedHash}`);
    }
  }

  if (errors.length > 0) {
    fail(errors);
  }
}

const skills = lockedSkills();
const projectPresent = containsLockedSkill(skills, "project");
const globalPresent = fs.existsSync(globalStampFile) || containsLockedSkill(skills, "global");

if (projectPresent && globalPresent) {
  fail([
    "error: common agent skills are installed in both this checkout's .agents/skills and ~/.agents/skills.",
    `Remove one copy with scripts/remove_common_skills --repo-root ${repoRoot} or scripts/remove_common_skills --repo-root ${repoRoot} --global, then rerun.`,
  ]);
}

if (!projectPresent && !globalPresent) {
  fail([
    "error: common agent skills are not installed in this checkout's .agents/skills or ~/.agents/skills.",
    `Install them with scripts/install_common_skills --repo-root ${repoRoot}, then rerun.`,
  ]);
}

if (target) {
  if (target !== "project" && target !== "global") {
    fail([`error: invalid common skills verification target: ${target} (expected project or global)`]);
  }

  if (target === "project" && !projectPresent) {
    fail(["error: common agent skills are not installed in this checkout's .agents/skills."]);
  }

  if (target === "global" && !globalPresent) {
    fail(["error: common agent skills are not installed in ~/.agents/skills."]);
  }
} else {
  target = projectPresent ? "project" : "global";
}

verifyTarget(skills, target);
if (!quiet) {
  console.log(`Common skills are installed in ${target === "global" ? "~/.agents/skills" : ".agents/skills"} and match skills-lock.json.`);
}
NODE
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --repo-root)
      if [[ -n "${2:-}" ]]; then
        REPO_ROOT="$2"
        shift 2
      else
        echo "error: argument for $1 is missing" >&2
        usage >&2
        exit 1
      fi
      ;;
    --project)
      TARGET="project"
      shift
      ;;
    --global)
      TARGET="global"
      shift
      ;;
    --if-needed)
      IF_NEEDED=1
      shift
      ;;
    --prompt-for-target)
      PROMPT_FOR_TARGET=1
      shift
      ;;
    --non-interactive)
      NON_INTERACTIVE=1
      shift
      ;;
    --force)
      FORCE=1
      shift
      ;;
    --quiet)
      QUIET=1
      shift
      ;;
    --verify-only)
      VERIFY_ONLY=1
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "error: unknown argument: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
done

if ! REPO_ROOT="$(resolve_repo_root "${REPO_ROOT}")"; then
  echo "error: repo root does not exist: ${REPO_ROOT}" >&2
  exit 1
fi
LOCK_FILE="${REPO_ROOT}/skills-lock.json"
if [[ "${WARP_SKIP_COMMON_SKILLS_INSTALL:-}" = "1" ]]; then
  log "Skipping common-skills install because WARP_SKIP_COMMON_SKILLS_INSTALL=1."
  exit 0
fi
if [[ "${VERIFY_ONLY}" -eq 1 ]]; then
  if [[ -n "${TARGET}" ]]; then
    if ! RESOLVED_TARGET="$(normalize_common_skills_target "${TARGET}")"; then
      echo "error: invalid common skills verification target: ${TARGET} (expected project or global)" >&2
      exit 1
    fi
    TARGET="${RESOLVED_TARGET}"
  fi
  if [[ ! -f "${LOCK_FILE}" ]]; then
    verify_common_skills
    exit 1
  fi
  validate_verify_dependencies

  fail_if_global_lock_mismatch
  verify_common_skills
  exit 0
fi
validate_install_dependencies
maybe_prompt_for_upstream_lock_update
if [[ "${FORCE}" -ne 1 && "${IF_NEEDED}" -eq 1 && -z "${TARGET}" ]] && common_skills_already_up_to_date_without_target; then
  log "Common skills are already up to date."
  exit 0
fi
fail_if_target_requires_unavailable_prompt
if ! RESOLVED_TARGET="$(resolve_common_skills_target)"; then
  echo "error: could not resolve common skills install target" >&2
  usage >&2
  exit 1
fi
TARGET="${RESOLVED_TARGET}"
if [[ ! -f "${LOCK_FILE}" ]]; then
  echo "Creating skills-lock.json from ${COMMON_SKILLS_REQUEST_SOURCE}."
  install_project_from_source
  if [[ ! -f "${LOCK_FILE}" ]]; then
    echo "error: expected ${LOCK_FILE} to be created by skills add" >&2
    exit 1
  fi
  fail_if_global_lock_mismatch

  if [[ "${TARGET}" = "global" ]]; then
    install_global_from_lock
    remove_project_skills_from_lock
  else
    ensure_project_common_skills_ignored
  fi

  LOCK_HASH="$(lock_hash)"
  STAMP_FILE="$(stamp_file)"
  mkdir -p "$(dirname "${STAMP_FILE}")"
  printf "%s\n" "${LOCK_HASH}" > "${STAMP_FILE}"
  verify_common_skills
  exit 0
fi

LOCK_HASH="$(lock_hash)"
STAMP_FILE="$(stamp_file)"
STAMP_HASH="$(stamp_hash "${STAMP_FILE}")"
fail_if_global_lock_mismatch
ensure_project_common_skills_ignored

if [[ "${FORCE}" -ne 1 && "${IF_NEEDED}" -eq 1 && "${STAMP_HASH}" = "${LOCK_HASH}" ]] && locked_skills_installed; then
  log "Common skills are already up to date."
  verify_common_skills
  exit 0
fi

echo "Installing common agent skills from skills-lock.json into $(target_description)."
install_from_lock
LOCK_HASH="$(lock_hash)"
mkdir -p "$(dirname "${STAMP_FILE}")"
printf "%s\n" "${LOCK_HASH}" > "${STAMP_FILE}"
verify_common_skills
