|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# config-patch: A tool for iterative JSON patching with high-security secret handling. |
| 4 | + |
| 5 | +# Determine script directory for relocatable operation |
| 6 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 7 | +SKILL_ROOT="$(dirname "$SCRIPT_DIR")" |
| 8 | + |
| 9 | +# Source shared config and functions |
| 10 | +source "$SCRIPT_DIR/config.sh" |
| 11 | + |
| 12 | +COMMAND="${1:-help}" |
| 13 | + |
| 14 | +case "$COMMAND" in |
| 15 | + |
| 16 | + start|sort) |
| 17 | + |
| 18 | + [ -f "$SECRETS_FILE" ] && { echo "Error: Session active. Commit or reset first."; exit 1; } |
| 19 | + [ ! -f "$PICOCLAW_CONFIG" ] && { echo "Error: $PICOCLAW_CONFIG not found."; exit 1; } |
| 20 | + |
| 21 | + redact_secrets || exit 1 |
| 22 | + |
| 23 | + # On success - Move results final locations (trap will clean if script crashes before this) |
| 24 | + # Clear <file>_TMP so trap doesn't try to delete final files |
| 25 | + |
| 26 | + mv "$CONFIG_TMP" "$STAGING_FILE" |
| 27 | + CONFIG_TMP="" |
| 28 | + mv "$SECRETS_TMP" "$SECRETS_FILE" |
| 29 | + SECRETS_TMP="" |
| 30 | + |
| 31 | + echo "Start: Redacted staging file created. Secrets mapped to $SECRETS_FILE" |
| 32 | + |
| 33 | + ;;& |
| 34 | + |
| 35 | + start) |
| 36 | + exit 0 |
| 37 | + ;; |
| 38 | + |
| 39 | + sort) |
| 40 | + |
| 41 | + { rm "$STAGING_FILE" ; jq -S . > "$STAGING_FILE"; } < "$STAGING_FILE" |
| 42 | + |
| 43 | + echo "Sorted: keys in $STAGING_FILE" |
| 44 | + exit 0 |
| 45 | + ;; |
| 46 | + |
| 47 | + commit) |
| 48 | + [ ! -f "$STAGING_FILE" ] && { echo "Error: No staged changes."; exit 1; } |
| 49 | + [ ! -f "$SECRETS_FILE" ] && { echo "Error: No secrets file found."; exit 1; } |
| 50 | + |
| 51 | + # 3. Restoration & Validation |
| 52 | + FINAL_FILE=$(mktemp) |
| 53 | + |
| 54 | + # Restore secrets: replace path placeholders with stored values |
| 55 | + # Keys in secrets file are the paths (e.g., "SECRET:agents.defaults.model") |
| 56 | + jq --slurpfile secrets "$SECRETS_FILE" 'walk(if type == "string" and $secrets[0][.] != null then $secrets[0][.] else . end)' "$STAGING_FILE" > "$FINAL_FILE" |
| 57 | + |
| 58 | + # Ensure no placeholders survived (means they weren't in the map) |
| 59 | + # Check if any string key in secrets map still exists in output |
| 60 | + while IFS= read -r key; do |
| 61 | + if grep -q "\"$key\"" "$FINAL_FILE"; then |
| 62 | + echo "Error: Placeholder '$key' was not restored. Check for typos in secrets map." |
| 63 | + exit 1 |
| 64 | + fi |
| 65 | + done < <(jq -r 'keys[]' "$SECRETS_FILE") |
| 66 | + |
| 67 | + # 4. JSON Integrity Check |
| 68 | + jq . "$FINAL_FILE" > /dev/null 2>&1 || { echo "Error: Invalid JSON output. Aborting."; exit 1; } |
| 69 | + |
| 70 | + # 5. Backup & Swap (unredacted config) |
| 71 | + mkdir -p "$BACKUP_DIR" |
| 72 | + cp "$PICOCLAW_CONFIG" "$BACKUP_DIR/${BASE}_${TIMESTAMP}.json" |
| 73 | + |
| 74 | + mv "$FINAL_FILE" "$PICOCLAW_CONFIG" |
| 75 | + rm -f "$STAGING_FILE" "$SECRETS_FILE" |
| 76 | + echo "Committed successfully. Backup: $BACKUP_DIR/${BASE}_${TIMESTAMP}.json" |
| 77 | + exit 0 |
| 78 | + ;; |
| 79 | + |
| 80 | + status) |
| 81 | + echo "Target: $PICOCLAW_CONFIG" |
| 82 | + [ -f "$STAGING_FILE" ] && echo "Staging: Active ($STAGING_FILE)" || echo "Staging: None" |
| 83 | + [ -f "$SECRETS_FILE" ] && echo "Secrets: $(jq 'keys | length' "$SECRETS_FILE") tracked" || echo "Secrets: None" |
| 84 | + ;; |
| 85 | + |
| 86 | + summary) |
| 87 | + # Output a summary of the staged config with only configured models, agents, enabled tools, devices, and heartbeat |
| 88 | + |
| 89 | + summarize "$STAGING_FILE" |
| 90 | + |
| 91 | + exit 0 |
| 92 | + ;; |
| 93 | + |
| 94 | + show|redacted|config) |
| 95 | + # Output the full staged config |
| 96 | + |
| 97 | + cat "$STAGING_FILE" |
| 98 | + ;; |
| 99 | + |
| 100 | + diff) |
| 101 | + [ ! -f "$STAGING_FILE" ] && { echo "Error: No staged changes to diff."; exit 1; } |
| 102 | + |
| 103 | + redact_secrets |
| 104 | + diff -u "$CONFIG_TMP" "$STAGING_FILE" |
| 105 | + exit 0 |
| 106 | + ;; |
| 107 | + |
| 108 | + rollback) |
| 109 | + # Restore the most recent backup |
| 110 | + LATEST=$(ls -t "$BACKUP_DIR"/${BASE}_*.json 2>/dev/null | head -n 1) |
| 111 | + if [ -z "$LATEST" ]; then |
| 112 | + echo "Error: No backups found for $PICOCLAW_CONFIG" |
| 113 | + exit 1 |
| 114 | + fi |
| 115 | + |
| 116 | + LATEST_BASE=$(basename "$LATEST" .json) |
| 117 | + |
| 118 | + cp "$LATEST" "$PICOCLAW_CONFIG" |
| 119 | + |
| 120 | + echo "Rollback: Restored $PICOCLAW_CONFIG from $LATEST" |
| 121 | + exit 0 |
| 122 | + ;; |
| 123 | + |
| 124 | + reset) |
| 125 | + rm -f "$STAGING_FILE" "$SECRETS_FILE" |
| 126 | + echo "Session cleared." |
| 127 | + exit 0 |
| 128 | + ;; |
| 129 | + |
| 130 | + help) |
| 131 | + echo "Usage: $0 <command> [config_file]" |
| 132 | + echo "Commands:" |
| 133 | + echo " start - Create staging file with redacted secrets" |
| 134 | + echo " sort - Sort JSON keys alphabetically" |
| 135 | + echo " diff - Show staged changes" |
| 136 | + echo " commit - Apply staged changes to config" |
| 137 | + echo " reset - Clear staging (discard changes)" |
| 138 | + echo " rollback - Restore from last backup" |
| 139 | + echo " status - Show current state" |
| 140 | + echo " summary - Show summary of the staged config with unused models/chatconfigs filtered" |
| 141 | + echo " config - Show the staged config" |
| 142 | + echo " <jq expr> - Apply inline jq patch (e.g. '.agents.model=\"gpt-4\"')" |
| 143 | + exit 0 |
| 144 | + ;; |
| 145 | + |
| 146 | + *) |
| 147 | + # Generic Patching |
| 148 | + redact_secrets |
| 149 | + if [ -f "$STAGING_FILE" ]; then |
| 150 | + SOURCE="$STAGING_FILE" |
| 151 | + else |
| 152 | + SOURCE="$PICOCLAW_CONFIG" |
| 153 | + fi |
| 154 | + TMP=$(mktemp) |
| 155 | + if jq "$COMMAND" "$SOURCE" > "$TMP"; then |
| 156 | + mv "$TMP" "$STAGING_FILE" |
| 157 | + echo "Applied patch to $STAGING_FILE" |
| 158 | + else |
| 159 | + rm -f "$TMP"; exit 1 |
| 160 | + fi |
| 161 | + ;; |
| 162 | +esac |
0 commit comments