Files
warpdotdev__common-skills/scripts/install_common_skills
2026-05-22 11:05:06 -06:00

984 lines
27 KiB
Bash
Executable File

#!/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