fix(skills): never prune shell installers' current-run backups

The four standalone installers pruned the oldest excess stamp
directories regardless of origin, so a migration retiring more than
five batches destroyed its own rollback material mid-run — the same
data loss already fixed for Go via the run-root registry and present
in install.js/install.ps1 as currentRunBackupRoots. Every installer now
records the stamp directories it creates and pruning only removes
earlier-run batches, which is what the changelog already promises.
This commit is contained in:
玉澜
2026-08-17 21:24:46 +08:00
parent 1d1aca5fd1
commit f7a3e606f7
5 changed files with 303 additions and 34 deletions
+27 -9
View File
@@ -95,8 +95,8 @@ refresh_cache_tree() {
backup_skill_dir() {
victim="$1"
[ -e "$victim" ] || [ -L "$victim" ] || { printf '\n'; return 0; }
# Prune before creating this run's archive, so the backup the caller is about
# to depend on for rollback can never be a prune candidate.
# Prune before creating this run's archive; every stamp this run has already
# created is registered, so pruning can only remove earlier-run batches.
prune_skill_backups
stamp="$(date -u +%Y%m%d-%H%M%S)"
name="$(printf '%s' "$victim" | sed 's#[/\\]#-#g; s#^-##')"
@@ -110,18 +110,34 @@ backup_skill_dir() {
return 1
fi
done
record_current_run_backup_stamp "$backup_root"
mkdir -p "$backup_root" || return 1
mv "$victim" "$backup" || return 1
printf '%s\n' "$backup"
}
# prune_skill_backups keeps only the newest SKILL_BACKUP_KEEP stamped backup
# directories under $HOME/.dws/skill-backups, matching Go's skillBackupKeep /
# pruneSkillBackups. Glob order is lexicographic and the stamps are UTC
# YYYYmmdd-HHMMSS, so the oldest sort first. Best-effort: a removal failure is
# reported and never fatal.
# directories from earlier runs under $HOME/.dws/skill-backups, matching Go's
# skillBackupKeep / pruneSkillBackups. Stamp directories created by the
# current process are never pruned (mirroring Go's run-root registry and
# install.js's currentRunBackupRoots), so a migration that retires more than
# SKILL_BACKUP_KEEP batches stays reversible. Glob order is lexicographic and
# the stamps are UTC YYYYmmdd-HHMMSS, so the oldest sort first. Best-effort:
# a removal failure is reported and never fatal.
SKILL_BACKUP_KEEP=5
# Stamp directories created by this run, recorded by backup_skill_dir so
# pruning can never delete a backup the running migration may still need to
# roll back to.
CURRENT_RUN_BACKUP_STAMPS=""
record_current_run_backup_stamp() {
case " $CURRENT_RUN_BACKUP_STAMPS " in
*" $1 "*) return 0 ;;
esac
CURRENT_RUN_BACKUP_STAMPS="${CURRENT_RUN_BACKUP_STAMPS} $1"
}
# is_skill_backup_stamp accepts only directory names DWS itself writes: UTC
# YYYYmmdd-HHMMSS, with an optional -N collision suffix. Any other entry in
# the backup root is foreign (user data, unrelated tooling) and is preserved
@@ -153,13 +169,15 @@ prune_skill_backups() {
done
[ "$prune_total" -gt "$SKILL_BACKUP_KEEP" ] || return 0
prune_drop=$((prune_total - SKILL_BACKUP_KEEP))
prune_seen=0
for prune_entry in "$prune_root"/*; do
[ "$prune_drop" -gt 0 ] || break
[ -d "$prune_entry" ] || continue
is_skill_backup_stamp "${prune_entry##*/}" || continue
prune_seen=$((prune_seen + 1))
[ "$prune_seen" -le "$prune_drop" ] || break
case " $CURRENT_RUN_BACKUP_STAMPS " in
*" $prune_entry "*) continue ;;
esac
rm -rf "$prune_entry" || printf ' ⚠️ 旧 Skill 备份清理失败: %s\n' "$prune_entry" >&2
prune_drop=$((prune_drop - 1))
done
}
+27 -9
View File
@@ -162,8 +162,8 @@ refresh_cache_tree() {
backup_skill_dir() {
victim="$1"
[ -e "$victim" ] || [ -L "$victim" ] || { printf '\n'; return 0; }
# Prune before creating this run's archive, so the backup the caller is about
# to depend on for rollback can never be a prune candidate.
# Prune before creating this run's archive; every stamp this run has already
# created is registered, so pruning can only remove earlier-run batches.
prune_skill_backups
stamp="$(date -u +%Y%m%d-%H%M%S)"
name="$(printf '%s' "$victim" | sed 's#[/\\]#-#g; s#^-##')"
@@ -179,18 +179,34 @@ backup_skill_dir() {
return 1
fi
done
record_current_run_backup_stamp "$backup_root"
mkdir -p "$backup_root" || return 1
mv "$victim" "$backup" || return 1
printf '%s\n' "$backup"
}
# prune_skill_backups keeps only the newest SKILL_BACKUP_KEEP stamped backup
# directories under $HOME/.dws/skill-backups, matching Go's skillBackupKeep /
# pruneSkillBackups. Glob order is lexicographic and the stamps are UTC
# YYYYmmdd-HHMMSS, so the oldest sort first. Best-effort: a removal failure is
# reported and never fatal.
# directories from earlier runs under $HOME/.dws/skill-backups, matching Go's
# skillBackupKeep / pruneSkillBackups. Stamp directories created by the
# current process are never pruned (mirroring Go's run-root registry and
# install.js's currentRunBackupRoots), so a migration that retires more than
# SKILL_BACKUP_KEEP batches stays reversible. Glob order is lexicographic and
# the stamps are UTC YYYYmmdd-HHMMSS, so the oldest sort first. Best-effort:
# a removal failure is reported and never fatal.
SKILL_BACKUP_KEEP=5
# Stamp directories created by this run, recorded by backup_skill_dir so
# pruning can never delete a backup the running migration may still need to
# roll back to.
CURRENT_RUN_BACKUP_STAMPS=""
record_current_run_backup_stamp() {
case " $CURRENT_RUN_BACKUP_STAMPS " in
*" $1 "*) return 0 ;;
esac
CURRENT_RUN_BACKUP_STAMPS="${CURRENT_RUN_BACKUP_STAMPS} $1"
}
# is_skill_backup_stamp accepts only directory names DWS itself writes: UTC
# YYYYmmdd-HHMMSS, with an optional -N collision suffix. Any other entry in
# the backup root is foreign (user data, unrelated tooling) and is preserved
@@ -222,13 +238,15 @@ prune_skill_backups() {
done
[ "$prune_total" -gt "$SKILL_BACKUP_KEEP" ] || return 0
prune_drop=$((prune_total - SKILL_BACKUP_KEEP))
prune_seen=0
for prune_entry in "$prune_root"/*; do
[ "$prune_drop" -gt 0 ] || break
[ -d "$prune_entry" ] || continue
is_skill_backup_stamp "${prune_entry##*/}" || continue
prune_seen=$((prune_seen + 1))
[ "$prune_seen" -le "$prune_drop" ] || break
case " $CURRENT_RUN_BACKUP_STAMPS " in
*" $prune_entry "*) continue ;;
esac
rm -rf "$prune_entry" || printf ' ⚠️ 旧 Skill 备份清理失败: %s\n' "$prune_entry" >&2
prune_drop=$((prune_drop - 1))
done
}
+25 -8
View File
@@ -67,6 +67,7 @@ backup_and_remove_skill_dir() {
return 1
fi
done
record_current_run_backup_stamp "$(dirname "$_bed_target")"
mkdir -p "$(dirname "$_bed_target")" 2>/dev/null || {
printf ' ⚠️ 无法创建备份目录,保留原目录 %s\n' "$_bed_dir"
return 1
@@ -80,13 +81,27 @@ backup_and_remove_skill_dir() {
return 1
}
# prune_skill_backups keeps only the newest DWS_SKILL_BACKUP_KEEP stamped backup
# directories under $HOME/.dws/skill-backups, matching Go's skillBackupKeep /
# pruneSkillBackups. Best-effort: a removal failure is reported, never fatal.
# Only safe to call once every backup manifest of this run has been consumed,
# because a single transaction can span more than one timestamp.
# prune_skill_backups keeps only the newest DWS_SKILL_BACKUP_KEEP stamped
# backup directories from earlier runs under $HOME/.dws/skill-backups,
# matching Go's skillBackupKeep / pruneSkillBackups. Stamp directories created
# by the current process are never pruned (mirroring Go's run-root registry
# and install.js's currentRunBackupRoots), so a migration that retires more
# than DWS_SKILL_BACKUP_KEEP batches stays reversible. Best-effort: a removal
# failure is reported, never fatal.
DWS_SKILL_BACKUP_KEEP=5
# Stamp directories created by this run, recorded by
# backup_and_remove_skill_dir so pruning can never delete a backup the
# running migration may still need to roll back to.
DWS_CURRENT_RUN_BACKUP_STAMPS=""
record_current_run_backup_stamp() {
case " $DWS_CURRENT_RUN_BACKUP_STAMPS " in
*" $1 "*) return 0 ;;
esac
DWS_CURRENT_RUN_BACKUP_STAMPS="${DWS_CURRENT_RUN_BACKUP_STAMPS} $1"
}
# is_skill_backup_stamp accepts only directory names DWS itself writes: UTC
# YYYYmmdd-HHMMSS, with an optional -N collision suffix. Any other entry in
# the backup root is foreign (user data, unrelated tooling) and is preserved
@@ -118,13 +133,15 @@ prune_skill_backups() {
done
[ "$_psb_total" -gt "$DWS_SKILL_BACKUP_KEEP" ] || return 0
_psb_drop=$((_psb_total - DWS_SKILL_BACKUP_KEEP))
_psb_seen=0
for _psb_entry in "$_psb_root"/*; do
[ "$_psb_drop" -gt 0 ] || break
[ -d "$_psb_entry" ] || continue
is_skill_backup_stamp "${_psb_entry##*/}" || continue
_psb_seen=$((_psb_seen + 1))
[ "$_psb_seen" -le "$_psb_drop" ] || break
case " $DWS_CURRENT_RUN_BACKUP_STAMPS " in
*" $_psb_entry "*) continue ;;
esac
rm -rf "$_psb_entry" || printf ' ⚠️ 旧 Skill 备份清理失败: %s\n' "$_psb_entry"
_psb_drop=$((_psb_drop - 1))
done
}
+25 -8
View File
@@ -85,6 +85,7 @@ backup_and_remove_skill_dir() {
return 1
fi
done
record_current_run_backup_stamp "$(dirname "$_bed_target")"
mkdir -p "$(dirname "$_bed_target")" 2>/dev/null || {
say " ⚠️ 无法创建备份目录,保留原目录 $_bed_dir"
return 1
@@ -98,13 +99,27 @@ backup_and_remove_skill_dir() {
return 1
}
# prune_skill_backups keeps only the newest DWS_SKILL_BACKUP_KEEP stamped backup
# directories under $HOME/.dws/skill-backups, matching Go's skillBackupKeep /
# pruneSkillBackups. Best-effort: a removal failure is reported, never fatal.
# Only safe to call once every backup manifest of this run has been consumed,
# because a single transaction can span more than one timestamp.
# prune_skill_backups keeps only the newest DWS_SKILL_BACKUP_KEEP stamped
# backup directories from earlier runs under $HOME/.dws/skill-backups,
# matching Go's skillBackupKeep / pruneSkillBackups. Stamp directories created
# by the current process are never pruned (mirroring Go's run-root registry
# and install.js's currentRunBackupRoots), so a migration that retires more
# than DWS_SKILL_BACKUP_KEEP batches stays reversible. Best-effort: a removal
# failure is reported, never fatal.
DWS_SKILL_BACKUP_KEEP=5
# Stamp directories created by this run, recorded by
# backup_and_remove_skill_dir so pruning can never delete a backup the
# running migration may still need to roll back to.
DWS_CURRENT_RUN_BACKUP_STAMPS=""
record_current_run_backup_stamp() {
case " $DWS_CURRENT_RUN_BACKUP_STAMPS " in
*" $1 "*) return 0 ;;
esac
DWS_CURRENT_RUN_BACKUP_STAMPS="${DWS_CURRENT_RUN_BACKUP_STAMPS} $1"
}
# is_skill_backup_stamp accepts only directory names DWS itself writes: UTC
# YYYYmmdd-HHMMSS, with an optional -N collision suffix. Any other entry in
# the backup root is foreign (user data, unrelated tooling) and is preserved
@@ -136,13 +151,15 @@ prune_skill_backups() {
done
[ "$_psb_total" -gt "$DWS_SKILL_BACKUP_KEEP" ] || return 0
_psb_drop=$((_psb_total - DWS_SKILL_BACKUP_KEEP))
_psb_seen=0
for _psb_entry in "$_psb_root"/*; do
[ "$_psb_drop" -gt 0 ] || break
[ -d "$_psb_entry" ] || continue
is_skill_backup_stamp "${_psb_entry##*/}" || continue
_psb_seen=$((_psb_seen + 1))
[ "$_psb_seen" -le "$_psb_drop" ] || break
case " $DWS_CURRENT_RUN_BACKUP_STAMPS " in
*" $_psb_entry "*) continue ;;
esac
rm -rf "$_psb_entry" || say " ⚠️ 旧 Skill 备份清理失败: $_psb_entry"
_psb_drop=$((_psb_drop - 1))
done
}