shellcheck / shellcheck (push) Failing after 19s
A sibling's version is its latest git tag (v1.4.0, or v1.4.0+3 for commits after it), or, without tags, its commit's date and short hash, like CoreDesktop's own. The version shown as available is that of the newest commit on the default branch, which is what update installs. sibling list shows each sibling's version. sibling check fetches every sibling at once and shows which have a newer version, and sibling update [name...] updates the named siblings or all of them. 71-siblings.sh now runs sibling update, so update and version share one way of updating. version lists the installed siblings with any newer version and, when run in a terminal, asks for each one whether to update it now. version --check and the update notification still only look at CoreDesktop. The sibling skill docs describe how to release a version by tagging it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
266 lines
7.7 KiB
Bash
Executable File
266 lines
7.7 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Purpose : Install and manage CoreDesktop sibling projects
|
|
#
|
|
# A sibling is a git repository cloned into ~/.local/share/siblings/<name>.
|
|
# If it has an install.sh, that runs once when the sibling is added.
|
|
# Every `update` pulls each sibling and runs its update.sh, if it has one
|
|
# (see updates.d/71-siblings.sh).
|
|
#
|
|
# A sibling's version is its latest git tag, e.g. v1.4.0, or v1.4.0+3 for three
|
|
# commits after it. A sibling without tags gets CoreDesktop's kind of version:
|
|
# the commit's date and short hash, e.g. 2026.11.02 (a1b2c3d).
|
|
|
|
SIBLINGS="$HOME/.local/share/siblings"
|
|
|
|
usage() {
|
|
cat << EOF
|
|
Usage: sibling [command]
|
|
|
|
add [url] Install a sibling project from a git repository (default)
|
|
list Show installed sibling projects and their versions
|
|
check Show which sibling projects have a newer version
|
|
update [name...] Update the named sibling projects, or all of them
|
|
remove <name> Uninstall a sibling project
|
|
EOF
|
|
}
|
|
|
|
# Version of the sibling in $1 at commit $2 (HEAD by default)
|
|
sibling_version() {
|
|
local dir="$1" rev="${2:-HEAD}" tag ahead
|
|
if tag="$(git -C "$dir" describe --tags --abbrev=0 "$rev" 2> /dev/null)"; then
|
|
ahead="$(git -C "$dir" rev-list --count "$tag..$rev" 2> /dev/null)"
|
|
if [[ "$ahead" =~ ^[0-9]+$ ]] && (( ahead > 0 )); then
|
|
echo "$tag+$ahead"
|
|
else
|
|
echo "$tag"
|
|
fi
|
|
else
|
|
git -C "$dir" log -1 --format='%cd (%h)' --date=format:%Y.%m.%d "$rev"
|
|
fi
|
|
}
|
|
|
|
# Installed siblings' directories, one per line, in alphabetical order
|
|
sibling_dirs() {
|
|
local dir
|
|
for dir in "$SIBLINGS"/*/; do
|
|
[ -d "$dir/.git" ] && echo "${dir%/}"
|
|
done
|
|
}
|
|
|
|
# Fetch the sibling in $1 and print "name<TAB>installed<TAB>available", where
|
|
# available is the version update would install, empty if it's up to date, or
|
|
# ? if the sibling's remote couldn't be checked.
|
|
check_one() {
|
|
local dir="$1" installed available="" behind
|
|
installed="$(sibling_version "$dir")"
|
|
if [ -n "$(git -C "$dir" status --porcelain --untracked-files=no 2> /dev/null)" ]; then
|
|
installed="$installed, modified"
|
|
fi
|
|
if ! GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$dir" fetch --quiet &> /dev/null \
|
|
|| ! behind="$(git -C "$dir" rev-list --count 'HEAD..@{upstream}' 2> /dev/null)"; then
|
|
available="?"
|
|
elif (( behind > 0 )); then
|
|
available="$(sibling_version "$dir" '@{upstream}')"
|
|
fi
|
|
printf '%s\t%s\t%s\n' "$(basename "$dir")" "$installed" "$available"
|
|
}
|
|
|
|
# check_one for every sibling, all at once so a slow remote doesn't hold up the
|
|
# rest, printed in alphabetical order. Prints nothing if none are installed.
|
|
check_all() {
|
|
local dirs dir i=0 tmp
|
|
mapfile -t dirs < <(sibling_dirs)
|
|
(( ${#dirs[@]} > 0 )) || return 0
|
|
tmp="$(mktemp -d)"
|
|
for dir in "${dirs[@]}"; do
|
|
check_one "$dir" > "$tmp/$i" &
|
|
i=$(( i + 1 ))
|
|
done
|
|
wait
|
|
for (( i = 0; i < ${#dirs[@]}; i++ )); do
|
|
cat "$tmp/$i"
|
|
done
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
# Print check_all's lines as a table. Returns 0 if any sibling has a newer
|
|
# version, 1 if none does.
|
|
show_checked() {
|
|
local name installed available newer=1 width=0 vwidth=0 lines=()
|
|
mapfile -t lines
|
|
for line in "${lines[@]}"; do
|
|
IFS=$'\t' read -r name installed _ <<< "$line"
|
|
(( ${#name} > width )) && width=${#name}
|
|
(( ${#installed} > vwidth )) && vwidth=${#installed}
|
|
done
|
|
for line in "${lines[@]}"; do
|
|
IFS=$'\t' read -r name installed available <<< "$line"
|
|
case "$available" in
|
|
"") available="up to date" ;;
|
|
"?") available="couldn't check" ;;
|
|
*) available="$available is available"; newer=0 ;;
|
|
esac
|
|
printf ' %-*s %-*s %s\n' "$width" "$name" "$vwidth" "$installed" "$available"
|
|
done
|
|
return $newer
|
|
}
|
|
|
|
check_siblings() {
|
|
if [ "$1" = "--porcelain" ]; then
|
|
check_all
|
|
return
|
|
fi
|
|
local checked
|
|
checked="$(check_all)"
|
|
if [ -z "$checked" ]; then
|
|
echo "No sibling projects installed."
|
|
return 1
|
|
fi
|
|
show_checked <<< "$checked"
|
|
}
|
|
|
|
# Pull each named sibling (all of them if none are named) and run its update.sh
|
|
update_siblings() {
|
|
local dirs=() dir name failed=0
|
|
if (( $# == 0 )); then
|
|
mapfile -t dirs < <(sibling_dirs)
|
|
fi
|
|
for name in "$@"; do
|
|
if [[ -z "$name" || "$name" == */* || "$name" == .* || ! -d "$SIBLINGS/$name/.git" ]]; then
|
|
echo "No sibling named '$name' is installed."
|
|
failed=1
|
|
continue
|
|
fi
|
|
dirs+=("$SIBLINGS/$name")
|
|
done
|
|
for dir in "${dirs[@]}"; do
|
|
name="$(basename "$dir")"
|
|
echo "Updating $name..."
|
|
if ! git -C "$dir" pull --ff-only --quiet; then
|
|
echo " Could not pull $name."
|
|
failed=1
|
|
continue
|
|
fi
|
|
if [ -f "$dir/update.sh" ]; then
|
|
(cd "$dir" && bash update.sh) || { echo " $name's update.sh failed."; failed=1; continue; }
|
|
fi
|
|
echo " $name is at $(sibling_version "$dir")."
|
|
done
|
|
return $failed
|
|
}
|
|
|
|
# Derive a directory name from a git URL, e.g.
|
|
# https://host/user/my-project.git -> my-project
|
|
sibling_name() {
|
|
local name="${1%/}"
|
|
name="${name##*/}"
|
|
name="${name##*:}"
|
|
echo "${name%.git}"
|
|
}
|
|
|
|
add_sibling() {
|
|
local url="$1" name dir
|
|
|
|
if [ -z "$url" ]; then
|
|
read -rp "Git repository URL for the sibling project: " url
|
|
fi
|
|
if [ -z "$url" ]; then
|
|
echo "No URL given."
|
|
return 1
|
|
fi
|
|
|
|
name="$(sibling_name "$url")"
|
|
if [[ -z "$name" || "$name" == .* ]]; then
|
|
echo "Could not work out a project name from: $url"
|
|
return 1
|
|
fi
|
|
dir="$SIBLINGS/$name"
|
|
|
|
mkdir -p "$SIBLINGS"
|
|
if [ -d "$dir/.git" ]; then
|
|
local existing
|
|
existing="$(git -C "$dir" remote get-url origin 2>/dev/null)"
|
|
if [ "$existing" != "$url" ]; then
|
|
echo "A different sibling named '$name' is already installed from: $existing"
|
|
return 1
|
|
fi
|
|
echo "$name is already installed; pulling the latest version..."
|
|
git -C "$dir" pull --ff-only || return 1
|
|
else
|
|
echo "Cloning $name..."
|
|
git clone "$url" "$dir" || return 1
|
|
fi
|
|
|
|
if [ -f "$dir/install.sh" ]; then
|
|
echo "Running $name's install.sh..."
|
|
if ! (cd "$dir" && bash install.sh); then
|
|
echo "$name's install.sh failed. Fix the problem and run 'sibling add $url' again."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Confirm the install
|
|
if [ -d "$dir/.git" ] && git -C "$dir" rev-parse --verify --quiet HEAD > /dev/null; then
|
|
echo "Installed $name $(sibling_version "$dir") at $dir."
|
|
echo "'update' will keep it up to date."
|
|
else
|
|
echo "$name does not look installed correctly at $dir."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
list_siblings() {
|
|
local dirs dir width=0 vwidth=0 versions=()
|
|
mapfile -t dirs < <(sibling_dirs)
|
|
if (( ${#dirs[@]} == 0 )); then
|
|
echo "No sibling projects installed."
|
|
return
|
|
fi
|
|
for dir in "${dirs[@]}"; do
|
|
versions+=("$(sibling_version "$dir")")
|
|
dir="$(basename "$dir")"
|
|
(( ${#dir} > width )) && width=${#dir}
|
|
done
|
|
for version in "${versions[@]}"; do
|
|
(( ${#version} > vwidth )) && vwidth=${#version}
|
|
done
|
|
for i in "${!dirs[@]}"; do
|
|
printf '%-*s %-*s %s
|
|
' "$width" "$(basename "${dirs[$i]}")" "$vwidth" "${versions[$i]}" \
|
|
"$(git -C "${dirs[$i]}" remote get-url origin 2> /dev/null)"
|
|
done
|
|
}
|
|
|
|
remove_sibling() {
|
|
local name="$1"
|
|
if [[ -z "$name" || "$name" == */* || "$name" == .* ]]; then
|
|
usage
|
|
return 1
|
|
fi
|
|
if [ ! -d "$SIBLINGS/$name" ]; then
|
|
echo "No sibling named '$name' is installed."
|
|
return 1
|
|
fi
|
|
read -rp "Remove $SIBLINGS/$name? [y/N] " response
|
|
case "$response" in
|
|
[yY][eE][sS]|[yY])
|
|
rm -rf "${SIBLINGS:?}/$name"
|
|
echo "Removed $name. Anything its install.sh set up outside that folder is still there."
|
|
;;
|
|
*)
|
|
echo "Nothing removed."
|
|
;;
|
|
esac
|
|
}
|
|
|
|
case "$1" in
|
|
""|add) add_sibling "$2" ;;
|
|
list|ls) list_siblings ;;
|
|
check) check_siblings "$2" ;;
|
|
update) shift; update_siblings "$@" ;;
|
|
remove|rm) remove_sibling "$2" ;;
|
|
-h|--help|help) usage ;;
|
|
*) usage; exit 1 ;;
|
|
esac
|