124 lines
4.4 KiB
Bash
Executable File
124 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# update-configs — sync dotfiles configs into ~/.config
|
|
#
|
|
# Config: ~/.config/config-updater/updater.conf
|
|
# Manifest: ~/.config/config-updater/manifest
|
|
|
|
set -euo pipefail
|
|
|
|
CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/config-updater"
|
|
CONF_FILE="${CONF_DIR}/updater.conf"
|
|
MANIFEST="${CONF_DIR}/manifest"
|
|
TARGET="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
|
|
RED='\033[0;31m'; YEL='\033[1;33m'; GRN='\033[0;32m'; DIM='\033[2m'; RST='\033[0m'
|
|
err() { printf "${RED}✖ %s${RST}\n" "$*" >&2; }
|
|
warn() { printf "${YEL}⚠ %s${RST}\n" "$*"; }
|
|
ok() { printf "${GRN}✔ %s${RST}\n" "$*"; }
|
|
note() { printf "${DIM} %s${RST}\n" "$*"; }
|
|
die() { err "$*"; exit 1; }
|
|
|
|
[[ -f "$CONF_FILE" ]] || die "Config not found: $CONF_FILE"
|
|
|
|
# ── parse updater.conf ────────────────────────────────────────────────────────
|
|
SOURCE_BASE=""
|
|
declare -A ENTRY_TYPE # name → config | flat | ignore
|
|
|
|
while IFS= read -r line; do
|
|
line="${line%%#*}"
|
|
line="${line#"${line%%[! ]*}"}"
|
|
line="${line%"${line##*[! ]}"}"
|
|
[[ -z "$line" ]] && continue
|
|
|
|
if [[ "$line" =~ ^SOURCE_BASE[[:space:]]*=[[:space:]]*(.+)$ ]]; then
|
|
src="${BASH_REMATCH[1]%"${BASH_REMATCH[1]##*[! ]}"}"
|
|
SOURCE_BASE="${src/#\~/$HOME}"
|
|
continue
|
|
fi
|
|
|
|
read -r type name _rest <<< "$line"
|
|
[[ -z "${name:-}" ]] && continue
|
|
case "$type" in
|
|
config|flat|ignore) ENTRY_TYPE["$name"]="$type" ;;
|
|
*) warn "Unknown type '$type' for '$name' — skipping" ;;
|
|
esac
|
|
done < "$CONF_FILE"
|
|
|
|
[[ -n "$SOURCE_BASE" ]] || die "SOURCE_BASE not defined in $CONF_FILE"
|
|
[[ -d "$SOURCE_BASE" ]] || die "SOURCE_BASE not found: $SOURCE_BASE"
|
|
|
|
# ── load previous manifest ────────────────────────────────────────────────────
|
|
declare -A PREV_MANIFEST
|
|
if [[ -f "$MANIFEST" ]]; then
|
|
while IFS=: read -r mtype mname; do
|
|
[[ -n "${mtype:-}" && -n "${mname:-}" ]] && PREV_MANIFEST["$mname"]="$mtype"
|
|
done < "$MANIFEST"
|
|
fi
|
|
|
|
warned=0
|
|
|
|
# ── warn: manifest entries no longer in updater.conf ─────────────────────────
|
|
for mname in "${!PREV_MANIFEST[@]}"; do
|
|
[[ -n "${ENTRY_TYPE[$mname]:-}" ]] && continue
|
|
warn "'$mname' was previously managed but is no longer in updater.conf"
|
|
note "Remove ~/.config/$mname manually if it is no longer needed"
|
|
warned=1
|
|
done
|
|
|
|
# ── warn: source items not covered by updater.conf ───────────────────────────
|
|
while IFS= read -r -d '' item; do
|
|
name="$(basename "$item")"
|
|
[[ "$name" == .* ]] && continue
|
|
[[ -n "${ENTRY_TYPE[$name]:-}" ]] && continue
|
|
warn "Untracked source item: '$name' — add to updater.conf (config / flat / ignore)"
|
|
warned=1
|
|
done < <(find "$SOURCE_BASE" -maxdepth 1 -mindepth 1 -print0 | sort -z)
|
|
|
|
(( warned )) && printf '\n'
|
|
|
|
# ── apply ─────────────────────────────────────────────────────────────────────
|
|
errors=0
|
|
new_manifest=()
|
|
|
|
for name in "${!ENTRY_TYPE[@]}"; do
|
|
type="${ENTRY_TYPE[$name]}"
|
|
[[ "$type" == ignore ]] && continue
|
|
|
|
src="${SOURCE_BASE}/${name}"
|
|
if [[ ! -e "$src" ]]; then
|
|
err "Source missing: $src"
|
|
(( errors++ )) || true
|
|
continue
|
|
fi
|
|
|
|
case "$type" in
|
|
config)
|
|
rm -rf "${TARGET:?}/${name}"
|
|
cp -r "$src" "$TARGET/$name"
|
|
ok "config $name"
|
|
;;
|
|
flat)
|
|
[[ -d "$src" ]] || {
|
|
err "flat entry '$name' must be a directory: $src"
|
|
(( errors++ )) || true
|
|
continue
|
|
}
|
|
cp -r "${src}/." "$TARGET/"
|
|
ok "flat $name → (contents into ~/.config/)"
|
|
;;
|
|
esac
|
|
|
|
new_manifest+=("${type}:${name}")
|
|
done
|
|
|
|
# ── write manifest ────────────────────────────────────────────────────────────
|
|
printf '%s\n' "${new_manifest[@]}" | sort > "$MANIFEST"
|
|
|
|
printf '\n'
|
|
if (( errors > 0 )); then
|
|
err "$errors error(s) — manifest may be incomplete"
|
|
exit 1
|
|
else
|
|
ok "Done — manifest updated at $MANIFEST"
|
|
fi
|