Files
pi-skills/.claude/skills/comet/scripts/comet-archive.sh
T

294 lines
6.7 KiB
Bash

#!/bin/bash
# Comet Archive — automates the archive phase in one command
# Usage: comet-archive.sh <change-name> [--dry-run]
# Exit 0 = archive complete, exit 1 = fatal error
set -euo pipefail
red() { echo -e "\033[31m$1\033[0m" >&2; }
green() { echo -e "\033[32m$1\033[0m" >&2; }
yellow() { echo -e "\033[33m$1\033[0m" >&2; }
DRY_RUN=0
if [[ "${2:-}" == "--dry-run" ]]; then
DRY_RUN=1
fi
# Input validation
validate_change_name() {
local name="$1"
if [ -z "$name" ]; then
red "FATAL: Change name cannot be empty"
exit 1
fi
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
red "FATAL: Invalid change name: '$name'"
red "Valid characters: a-z, A-Z, 0-9, -, _"
exit 1
fi
if [[ "$name" =~ \.\. ]]; then
red "FATAL: Change name cannot contain '..'"
exit 1
fi
}
CHANGE="$1"
validate_change_name "$CHANGE"
CHANGE_DIR="openspec/changes/$CHANGE"
YAML="$CHANGE_DIR/.comet.yaml"
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")" && pwd)"
STATE_SH="$SCRIPT_DIR/comet-state.sh"
TODAY=$(date +%Y-%m-%d)
ARCHIVE_NAME="${TODAY}-${CHANGE}"
ARCHIVE_DIR="openspec/changes/archive/${ARCHIVE_NAME}"
STEPS_OK=0
STEPS_TOTAL=0
step_ok() {
green " [OK] $1"
STEPS_OK=$((STEPS_OK + 1))
STEPS_TOTAL=$((STEPS_TOTAL + 1))
}
step_fail() {
red " [FAIL] $1"
STEPS_TOTAL=$((STEPS_TOTAL + 1))
}
step_dry_run() {
yellow " [DRY-RUN] $1"
STEPS_OK=$((STEPS_OK + 1))
STEPS_TOTAL=$((STEPS_TOTAL + 1))
}
echo "=== Comet Archive: $CHANGE ===" >&2
# --- Step 1: Read .comet.yaml, extract paths ---
yaml_field() {
local field="$1"
if [ -f "$STATE_SH" ]; then
bash "$STATE_SH" get "$CHANGE" "$field" 2>/dev/null
else
if [ -f "$YAML" ]; then
local value
value=$(grep "^${field}:" "$YAML" 2>/dev/null | sed "s/^${field}: *//" || true)
value=$(strip_inline_comment "$value")
strip_wrapping_quotes "$value"
fi
fi
}
strip_inline_comment() {
local value="$1"
printf '%s\n' "$value" | awk -v squote="'" '
{
out = ""
quote = ""
for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1)
if (quote == "") {
if (c == "\"" || c == squote) {
quote = c
} else if (c == "#" && (i == 1 || substr($0, i - 1, 1) ~ /[[:space:]]/)) {
sub(/[[:space:]]+$/, "", out)
print out
next
}
} else if (c == quote) {
quote = ""
}
out = out c
}
print out
}
'
}
strip_wrapping_quotes() {
local value="$1"
case "$value" in
\"*\") printf '%s\n' "${value:1:${#value}-2}" ;;
\'*\') printf '%s\n' "${value:1:${#value}-2}" ;;
*) printf '%s\n' "$value" ;;
esac
}
if [ ! -f "$YAML" ]; then
red "FATAL: .comet.yaml not found in $CHANGE_DIR/"
exit 1
fi
DESIGN_DOC=$(yaml_field "design_doc")
PLAN_PATH=$(yaml_field "plan")
# --- Step 2: Validate entry state ---
PHASE_VAL=$(yaml_field "phase")
VERIFY_VAL=$(yaml_field "verify_result")
ARCHIVED_VAL=$(yaml_field "archived")
if [ "$PHASE_VAL" != "archive" ]; then
red "FATAL: phase is '$PHASE_VAL', expected 'archive'"
exit 1
fi
if [ "$VERIFY_VAL" != "pass" ]; then
red "FATAL: verify_result is '$VERIFY_VAL', expected 'pass'. Run comet-verify first."
exit 1
fi
if [ "$ARCHIVED_VAL" = "true" ]; then
red "FATAL: change already archived"
exit 1
fi
step_ok "Entry state verified"
# --- Step 3: Check archive target ---
if [ -d "$ARCHIVE_DIR" ]; then
red "FATAL: archive target already exists: $ARCHIVE_DIR"
exit 1
fi
step_ok "Archive target available"
# --- Step 4: Sync delta specs → main specs ---
sync_delta_specs() {
local delta_root="$CHANGE_DIR/specs"
if [ ! -d "$delta_root" ]; then
return 0
fi
for delta_spec_dir in "$delta_root"/*/; do
[ -d "$delta_spec_dir" ] || continue
local capability
capability=$(basename "$delta_spec_dir")
local delta_spec="$delta_spec_dir/spec.md"
local main_spec="openspec/specs/$capability/spec.md"
if [ ! -f "$delta_spec" ]; then
continue
fi
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would sync: $capability$main_spec"
continue
fi
if [ ! -f "$main_spec" ]; then
mkdir -p "openspec/specs/$capability"
elif ! cmp -s "$main_spec" "$delta_spec"; then
yellow " [DIFF] Delta spec differs from main spec before sync: $capability"
diff -u "$main_spec" "$delta_spec" >&2 || true
fi
cp "$delta_spec" "$main_spec"
step_ok "Delta spec synced: $capability → openspec/specs/$capability/spec.md"
done
}
sync_delta_specs
# --- Step 5: Annotate design doc frontmatter ---
annotate_frontmatter() {
local file="$1"
local extra_fields="$2"
if [ ! -f "$file" ]; then
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would annotate: $file"
return 0
fi
if head -1 "$file" | grep -q '^---'; then
local tmp_file
tmp_file=$(mktemp)
awk -v archive="$ARCHIVE_NAME" -v extra="$extra_fields" '
/^archived-with:/ { next }
NR==1 && /^---/ { print; next }
/^---/ && NR>1 {
print "archived-with: " archive
if (extra != "") print extra
print; next
}
{ print }
' "$file" > "$tmp_file"
mv "$tmp_file" "$file"
else
local tmp_file
tmp_file=$(mktemp)
{
echo "---"
echo "archived-with: $ARCHIVE_NAME"
if [ -n "$extra_fields" ]; then
echo "$extra_fields"
fi
echo "status: final"
echo "---"
cat "$file"
} > "$tmp_file"
mv "$tmp_file" "$file"
fi
step_ok "Annotated: $file"
}
if [ -n "$DESIGN_DOC" ] && [ "$DESIGN_DOC" != "null" ]; then
annotate_frontmatter "$DESIGN_DOC" "status: final"
fi
# --- Step 6: Annotate plan frontmatter ---
if [ -n "$PLAN_PATH" ] && [ "$PLAN_PATH" != "null" ]; then
annotate_frontmatter "$PLAN_PATH" ""
fi
# --- Step 7: Move change to archive ---
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would move: $CHANGE_DIR$ARCHIVE_DIR"
else
mkdir -p "openspec/changes/archive"
mv "$CHANGE_DIR" "$ARCHIVE_DIR"
step_ok "Moved to: $ARCHIVE_DIR"
fi
# --- Step 8: Mark archived via comet-state transition ---
ARCHIVE_YAML="$ARCHIVE_DIR/.comet.yaml"
if [ "$DRY_RUN" -eq 1 ]; then
step_dry_run "Would set archived: true in $ARCHIVE_YAML"
else
if [ -f "$ARCHIVE_YAML" ]; then
bash "$STATE_SH" transition "$ARCHIVE_NAME" archived >/dev/null
step_ok "archived: true"
else
step_fail "archived: true (.comet.yaml not found after move)"
fi
fi
# --- Step 9: Print summary ---
echo "" >&2
if [ "$DRY_RUN" -eq 1 ]; then
yellow "Dry run complete. $STEPS_OK/$STEPS_TOTAL steps would succeed."
else
green "Archive complete. $STEPS_OK/$STEPS_TOTAL steps succeeded."
fi
if [ "$STEPS_OK" -lt "$STEPS_TOTAL" ]; then
exit 1
fi
exit 0