234 lines
8.2 KiB
Bash
234 lines
8.2 KiB
Bash
#!/bin/bash
|
|||
|
|
# ==============================================================================
|
||
|
|
# OpenCode Persistence Save Script
|
||
|
|
# ==============================================================================
|
||
|
|
# This script prepares all session data for artifact upload.
|
||
|
|
# It saves:
|
||
|
|
# - Configuration files (~/.config/opencode/)
|
||
|
|
# - Session data, messages, chats (~/.local/share/opencode/storage/)
|
||
|
|
# - Project snapshots (~/.local/share/opencode/snapshot/)
|
||
|
|
# - Authentication tokens (~/.local/share/opencode/auth.json)
|
||
|
|
# - Logs (~/.local/share/opencode/log/)
|
||
|
|
#
|
||
|
|
# Excludes (to keep artifact size manageable):
|
||
|
|
# - node_modules/ directories
|
||
|
|
# - bin/ directory (will be reinstalled)
|
||
|
|
# - tool-output/ (temporary tool outputs)
|
||
|
|
#
|
||
|
|
# If OPENCODE_SERVER_PASSWORD is set, the artifact will be encrypted.
|
||
|
|
# ==============================================================================
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
SAVE_DIR="${SAVE_DIR:-/tmp/opencode-save}"
|
||
|
|
CONFIG_DIR="$HOME/.config/opencode"
|
||
|
|
SHARE_DIR="$HOME/.local/share/opencode"
|
||
|
|
ENCRYPTION_PASSWORD="${OPENCODE_SERVER_PASSWORD:-}"
|
||
|
|
|
||
|
|
echo "=== OpenCode Session Save ==="
|
||
|
|
echo "Save directory: $SAVE_DIR"
|
||
|
|
echo "Config directory: $CONFIG_DIR"
|
||
|
|
echo "Share directory: $SHARE_DIR"
|
||
|
|
if [ -n "$ENCRYPTION_PASSWORD" ]; then
|
||
|
|
echo "Encryption: ENABLED"
|
||
|
|
else
|
||
|
|
echo "Encryption: DISABLED (set OPENCODE_SERVER_PASSWORD to enable)"
|
||
|
|
fi
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
# Clean up and create save directory
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
rm -rf "$SAVE_DIR"
|
||
|
|
mkdir -p "$SAVE_DIR/config"
|
||
|
|
mkdir -p "$SAVE_DIR/share"
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
# Save configuration files
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
save_config() {
|
||
|
|
echo "=== Saving Configuration Files ==="
|
||
|
|
|
||
|
|
if [ -d "$CONFIG_DIR" ] && [ "$(ls -A "$CONFIG_DIR" 2>/dev/null)" ]; then
|
||
|
|
# Save all config files (excluding node_modules)
|
||
|
|
for item in "$CONFIG_DIR"/*; do
|
||
|
|
if [ -e "$item" ]; then
|
||
|
|
local basename=$(basename "$item")
|
||
|
|
|
||
|
|
# Skip node_modules and bun.lock (will be reinstalled)
|
||
|
|
if [ "$basename" = "node_modules" ] || [ "$basename" = "bun.lock" ]; then
|
||
|
|
echo "Skipping: $basename (will be reinstalled)"
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Copy file or directory
|
||
|
|
if [ -f "$item" ]; then
|
||
|
|
cp -v "$item" "$SAVE_DIR/config/" 2>/dev/null || true
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
local count=$(find "$SAVE_DIR/config" -type f 2>/dev/null | wc -l)
|
||
|
|
echo "Config files saved: $count"
|
||
|
|
else
|
||
|
|
echo "No config data to save"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
# Save share data (sessions, messages, snapshots, auth)
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
save_share() {
|
||
|
|
echo ""
|
||
|
|
echo "=== Saving Share Data ==="
|
||
|
|
|
||
|
|
if [ ! -d "$SHARE_DIR" ]; then
|
||
|
|
echo "No share directory found"
|
||
|
|
return
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Save auth.json (authentication tokens - critical!)
|
||
|
|
if [ -f "$SHARE_DIR/auth.json" ]; then
|
||
|
|
cp -v "$SHARE_DIR/auth.json" "$SAVE_DIR/share/" 2>/dev/null || true
|
||
|
|
echo "Auth data saved"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Save storage directory (sessions, messages, parts, projects)
|
||
|
|
if [ -d "$SHARE_DIR/storage" ] && [ "$(ls -A "$SHARE_DIR/storage" 2>/dev/null)" ]; then
|
||
|
|
echo "Saving storage data (sessions, messages, chats)..."
|
||
|
|
mkdir -p "$SAVE_DIR/share/storage"
|
||
|
|
|
||
|
|
# Copy all storage subdirectories
|
||
|
|
for subdir in message migration part project session session_diff; do
|
||
|
|
if [ -d "$SHARE_DIR/storage/$subdir" ]; then
|
||
|
|
cp -r "$SHARE_DIR/storage/$subdir" "$SAVE_DIR/share/storage/" 2>/dev/null || true
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
local storage_count=$(find "$SAVE_DIR/share/storage" -type f 2>/dev/null | wc -l)
|
||
|
|
echo "Storage files saved: $storage_count"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Save snapshot directory (project snapshots for undo/rollback)
|
||
|
|
if [ -d "$SHARE_DIR/snapshot" ] && [ "$(ls -A "$SHARE_DIR/snapshot" 2>/dev/null)" ]; then
|
||
|
|
echo "Saving snapshot data..."
|
||
|
|
mkdir -p "$SAVE_DIR/share/snapshot"
|
||
|
|
cp -r "$SHARE_DIR/snapshot/"* "$SAVE_DIR/share/snapshot/" 2>/dev/null || true
|
||
|
|
|
||
|
|
local snapshot_count=$(find "$SAVE_DIR/share/snapshot" -type f 2>/dev/null | wc -l)
|
||
|
|
echo "Snapshot files saved: $snapshot_count"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Save log directory (useful for debugging)
|
||
|
|
if [ -d "$SHARE_DIR/log" ] && [ "$(ls -A "$SHARE_DIR/log" 2>/dev/null)" ]; then
|
||
|
|
echo "Saving log data..."
|
||
|
|
mkdir -p "$SAVE_DIR/share/log"
|
||
|
|
cp -r "$SHARE_DIR/log/"* "$SAVE_DIR/share/log/" 2>/dev/null || true
|
||
|
|
echo "Log files saved"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
# Create manifest with metadata
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
create_manifest() {
|
||
|
|
echo ""
|
||
|
|
echo "=== Creating Manifest ==="
|
||
|
|
|
||
|
|
local manifest="$SAVE_DIR/manifest.json"
|
||
|
|
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||
|
|
local config_count=$(find "$SAVE_DIR/config" -type f 2>/dev/null | wc -l)
|
||
|
|
local share_count=$(find "$SAVE_DIR/share" -type f 2>/dev/null | wc -l)
|
||
|
|
local total_size=$(du -sh "$SAVE_DIR" 2>/dev/null | cut -f1)
|
||
|
|
|
||
|
|
cat << EOF > "$manifest"
|
||
|
|
{
|
||
|
|
"version": "2.0",
|
||
|
|
"timestamp": "$timestamp",
|
||
|
|
"hostname": "$(hostname)",
|
||
|
|
"stats": {
|
||
|
|
"config_files": $config_count,
|
||
|
|
"share_files": $share_count,
|
||
|
|
"total_size": "$total_size"
|
||
|
|
},
|
||
|
|
"contents": {
|
||
|
|
"config": $([ -d "$SAVE_DIR/config" ] && ls "$SAVE_DIR/config" 2>/dev/null | jq -R -s -c 'split("\n") | map(select(length > 0))' || echo '[]'),
|
||
|
|
"share": $([ -d "$SAVE_DIR/share" ] && ls "$SAVE_DIR/share" 2>/dev/null | jq -R -s -c 'split("\n") | map(select(length > 0))' || echo '[]')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
|
||
|
|
echo "Manifest created: $manifest"
|
||
|
|
cat "$manifest"
|
||
|
|
}
|
||
|
|
|
||
|
|
# ==============================================================================
|
||
|
|
# Main Save Process
|
||
|
|
# ==============================================================================
|
||
|
|
|
||
|
|
save_config
|
||
|
|
save_share
|
||
|
|
create_manifest
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
# Final Summary
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
echo ""
|
||
|
|
echo "=== Save Summary ==="
|
||
|
|
|
||
|
|
# Calculate totals
|
||
|
|
total_files=$(find "$SAVE_DIR" -type f 2>/dev/null | wc -l)
|
||
|
|
total_size=$(du -sh "$SAVE_DIR" 2>/dev/null | cut -f1)
|
||
|
|
|
||
|
|
echo "Save directory structure:"
|
||
|
|
if command -v tree &> /dev/null; then
|
||
|
|
tree -L 2 "$SAVE_DIR" 2>/dev/null || ls -laR "$SAVE_DIR" | head -50
|
||
|
|
else
|
||
|
|
ls -laR "$SAVE_DIR" | head -50
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
echo "Total files to upload: $total_files"
|
||
|
|
echo "Total size: $total_size"
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# Verify we have something to save
|
||
|
|
if [ "$total_files" -gt 1 ]; then
|
||
|
|
echo "Session data prepared successfully for artifact upload!"
|
||
|
|
else
|
||
|
|
echo "Warning: Minimal data to save. Creating placeholder..."
|
||
|
|
echo "{\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"status\": \"empty\"}" > "$SAVE_DIR/placeholder.json"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
# Optional Encryption (if OPENCODE_SERVER_PASSWORD is set)
|
||
|
|
# ------------------------------------------------------------------------------
|
||
|
|
if [ -n "$ENCRYPTION_PASSWORD" ]; then
|
||
|
|
echo ""
|
||
|
|
echo "=== Encrypting Session Data ==="
|
||
|
|
|
||
|
|
TEMP_ARCHIVE="/tmp/opencode-session-data.tar.gz"
|
||
|
|
ENCRYPTED_FILE="$SAVE_DIR.enc"
|
||
|
|
|
||
|
|
tar -czf "$TEMP_ARCHIVE" -C "$SAVE_DIR" .
|
||
|
|
|
||
|
|
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 \
|
||
|
|
-in "$TEMP_ARCHIVE" \
|
||
|
|
-out "$ENCRYPTED_FILE" \
|
||
|
|
-pass pass:"$ENCRYPTION_PASSWORD"
|
||
|
|
|
||
|
|
rm -f "$TEMP_ARCHIVE"
|
||
|
|
rm -rf "$SAVE_DIR"
|
||
|
|
mkdir -p "$SAVE_DIR"
|
||
|
|
mv "$ENCRYPTED_FILE" "$SAVE_DIR/session.enc"
|
||
|
|
|
||
|
|
echo '{"encrypted": true, "algorithm": "aes-256-cbc", "kdf": "pbkdf2", "iterations": 100000}' > "$SAVE_DIR/manifest.json"
|
||
|
|
|
||
|
|
echo "Encryption complete. Artifact is password-protected."
|
||
|
|
echo "Encrypted file: $SAVE_DIR/session.enc"
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
echo "Save location: $SAVE_DIR"
|
||
|
|
echo "Ready for artifact upload!"
|