cortexhawk 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,41 +1,47 @@
1
1
  #!/bin/bash
2
- # post-merge-cleanup.sh — Delete merged branches and optionally create new feature branch
2
+ # post-merge-cleanup.sh — Unified post-merge cleanup for all git workflow strategies
3
+ # Strategies: direct-main, feature-branches, dev-branch, gitflow
3
4
  # Called by /cleanup command and post-merge hook
4
- # Args: --auto (silent mode, no prompts, skip remote deletion)
5
+ # Args: --auto (silent mode, no prompts), --dry-run (preview without executing)
5
6
 
6
7
  set -e
7
8
 
8
- # Parse arguments
9
+ # --- 1. FLAGS ---
9
10
  AUTO_MODE=false
10
- [ "$1" = "--auto" ] && AUTO_MODE=true
11
- [ ! -t 0 ] && AUTO_MODE=true # No TTY (Claude Bash tool, CI, pipe) → auto
11
+ DRY_RUN=false
12
+ [ ! -t 0 ] && AUTO_MODE=true
13
+ for arg in "$@"; do
14
+ case "$arg" in
15
+ --auto) AUTO_MODE=true ;;
16
+ --dry-run) DRY_RUN=true ;;
17
+ esac
18
+ done
12
19
 
13
- # Validate git repository
20
+ # --- 2. GIT VALIDATION ---
14
21
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
15
22
  echo "Error: not a git repository"
16
23
  exit 1
17
24
  fi
18
25
 
19
- # --- Branching detection with fallback layers ---
26
+ # run_cmd: execute command or print preview in dry-run mode
27
+ run_cmd() {
28
+ if [ "$DRY_RUN" = true ]; then echo "[dry-run] $*"; else "$@"; fi
29
+ }
30
+ log() { [ "$AUTO_MODE" = false ] && echo "$1" || true; }
31
+
32
+ # --- 3. STRATEGY DETECTION (layers 1-3) ---
20
33
  BRANCHING="direct-main"
21
34
  MAIN_BRANCH="main"
22
35
  WORK_BRANCH=""
23
36
 
24
- # Layer 1: Read .claude/git-workflow.conf (safe parsing, no source)
37
+ # Layer 1: .claude/git-workflow.conf (safe parsing, no source)
25
38
  if [ -f ".claude/git-workflow.conf" ]; then
26
39
  BRANCHING=$(grep -E '^BRANCHING=' ".claude/git-workflow.conf" | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'")
27
40
  WORK_BRANCH=$(grep -E '^WORK_BRANCH=' ".claude/git-workflow.conf" | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'")
28
-
29
41
  case "$BRANCHING" in
30
- dev-branch)
31
- MAIN_BRANCH="${WORK_BRANCH:-dev}"
32
- ;;
33
- gitflow)
34
- MAIN_BRANCH="develop"
35
- ;;
36
- feature-branches|direct-main)
37
- MAIN_BRANCH="main"
38
- ;;
42
+ dev-branch) MAIN_BRANCH="main" ;;
43
+ gitflow) MAIN_BRANCH="main"; WORK_BRANCH="${WORK_BRANCH:-develop}" ;;
44
+ feature-branches|direct-main) MAIN_BRANCH="main" ;;
39
45
  esac
40
46
  fi
41
47
 
@@ -47,97 +53,181 @@ if [ ! -f ".claude/git-workflow.conf" ] && [ -f "CLAUDE.md" ]; then
47
53
  dev-branch)
48
54
  BRANCHING="dev-branch"
49
55
  WORK_BRANCH=$(grep -i "working branch:" "CLAUDE.md" | head -1 | sed 's/.*: //' | tr -d ')')
50
- MAIN_BRANCH="${WORK_BRANCH:-dev}"
51
- ;;
52
- gitflow)
53
- BRANCHING="gitflow"
54
- MAIN_BRANCH="develop"
55
- ;;
56
- feature-branches)
57
- BRANCHING="feature-branches"
58
- MAIN_BRANCH="main"
59
- ;;
60
- direct-main)
61
- BRANCHING="direct-main"
62
56
  MAIN_BRANCH="main"
63
57
  ;;
58
+ gitflow) BRANCHING="gitflow"; MAIN_BRANCH="main"; WORK_BRANCH="${WORK_BRANCH:-develop}" ;;
59
+ feature-branches) BRANCHING="feature-branches"; MAIN_BRANCH="main" ;;
60
+ direct-main) BRANCHING="direct-main"; MAIN_BRANCH="main" ;;
64
61
  esac
65
62
  fi
66
63
  fi
67
64
 
68
- # Layer 3: Git-based fallback (detect main vs master)
65
+ # Layer 3: verify MAIN_BRANCH exists (main vs master fallback)
69
66
  if ! git rev-parse --verify "$MAIN_BRANCH" >/dev/null 2>&1; then
70
67
  if git rev-parse --verify main >/dev/null 2>&1; then
71
68
  MAIN_BRANCH="main"
72
69
  elif git rev-parse --verify master >/dev/null 2>&1; then
73
70
  MAIN_BRANCH="master"
74
71
  else
75
- echo "Error: branch $MAIN_BRANCH does not exist"
76
- exit 1
72
+ echo "Error: branch $MAIN_BRANCH does not exist"; exit 1
77
73
  fi
78
74
  fi
79
75
 
80
- [ "$AUTO_MODE" = false ] && echo "Detected branching: $BRANCHING, main branch: $MAIN_BRANCH"
81
-
82
- # --- Merged branch detection ---
83
- CURRENT_BRANCH=$(git branch --show-current)
84
- MERGED_BRANCHES=$(git branch --merged "$MAIN_BRANCH" 2>/dev/null | grep -v '^\*' | grep -vE '(main|master|dev|develop)$' | sed 's/^[[:space:]]*//' || true)
85
-
86
- if [ -z "$MERGED_BRANCHES" ]; then
87
- [ "$AUTO_MODE" = false ] && echo "No merged branches to clean up"
88
- exit 0
89
- fi
90
-
91
- BRANCH_COUNT=$(echo "$MERGED_BRANCHES" | wc -l | tr -d ' ')
92
- [ "$AUTO_MODE" = false ] && echo "Found $BRANCH_COUNT merged branch(es): $(echo "$MERGED_BRANCHES" | tr '\n' ' ')"
76
+ # --- 4. PROTECTED BRANCHES ---
77
+ # Single source of truth — these branches are never deleted
78
+ PROTECTED_BRANCHES="main master dev develop ${WORK_BRANCH}"
79
+
80
+ is_protected() {
81
+ local branch="$1"
82
+ for p in $PROTECTED_BRANCHES; do
83
+ [ "$branch" = "$p" ] && return 0
84
+ done
85
+ # Non-gitflow: protect release/hotfix patterns as safety net
86
+ # Gitflow: allow deletion of merged release/hotfix branches
87
+ if [ "$BRANCHING" != "gitflow" ]; then
88
+ case "$branch" in release/*|hotfix/*) return 0 ;; esac
89
+ fi
90
+ return 1
91
+ }
93
92
 
94
- # --- Check for remote ---
93
+ # --- 5. REMOTE CHECK ---
95
94
  REMOTE_EXISTS=true
96
95
  if ! git remote get-url origin >/dev/null 2>&1; then
97
96
  REMOTE_EXISTS=false
98
- [ "$AUTO_MODE" = false ] && echo "Warning: no remote 'origin' configured, skipping remote operations"
97
+ log "Warning: no remote 'origin' configured, skipping remote operations"
99
98
  fi
99
+ CURRENT_BRANCH=$(git branch --show-current)
100
+ log "Strategy: $BRANCHING | main: $MAIN_BRANCH | work: ${WORK_BRANCH:-(none)}"
100
101
 
101
- # --- Delete merged branches ---
102
- while IFS= read -r branch; do
103
- [ -z "$branch" ] && continue
102
+ # --- 6. HELPERS ---
104
103
 
105
- # Local deletion
104
+ delete_branch() {
105
+ local branch="$1"
106
+ is_protected "$branch" && { log " Protected, skipping: $branch"; return 0; }
106
107
  if [ "$AUTO_MODE" = true ]; then
107
- git branch -d "$branch" 2>/dev/null || echo "Failed to delete $branch (may have unmerged changes)"
108
+ run_cmd git branch -d "$branch" 2>/dev/null || log " Could not delete $branch"
108
109
  else
109
- read -r -p "Delete local branch '$branch'? [y/N]: " confirm
110
- if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
111
- git branch -d "$branch" || echo "Failed to delete $branch (may have unmerged changes)"
112
- fi
110
+ read -r -p "Delete local branch '$branch'? [y/N]: " c
111
+ { [ "$c" = "y" ] || [ "$c" = "Y" ]; } && \
112
+ { run_cmd git branch -d "$branch" || log " Failed to delete $branch"; }
113
113
  fi
114
-
115
- # Remote deletion (skip in auto mode)
116
114
  if [ "$AUTO_MODE" = false ] && [ "$REMOTE_EXISTS" = true ]; then
117
115
  if git rev-parse --verify "refs/remotes/origin/$branch" >/dev/null 2>&1; then
118
- read -r -p "Delete remote branch 'origin/$branch'? [y/N]: " confirm_remote
119
- if [ "$confirm_remote" = "y" ] || [ "$confirm_remote" = "Y" ]; then
120
- git push origin --delete "$branch" 2>/dev/null || echo "Failed to delete remote branch $branch (may not exist or no permissions)"
116
+ read -r -p "Delete remote 'origin/$branch'? [y/N]: " cr
117
+ { [ "$cr" = "y" ] || [ "$cr" = "Y" ]; } && \
118
+ { run_cmd git push origin --delete "$branch" 2>/dev/null || log " Failed to delete remote $branch"; }
119
+ fi
120
+ fi
121
+ }
122
+
123
+ delete_merged_branches() {
124
+ local target="${1:-$MAIN_BRANCH}"
125
+ local merged
126
+ merged=$(git branch --merged "$target" 2>/dev/null \
127
+ | grep -v '^\*' | sed 's/^[[:space:]]*//' || true)
128
+ if [ -z "$merged" ]; then
129
+ log "No merged branches to clean up"
130
+ return 0
131
+ fi
132
+ local count; count=$(echo "$merged" | wc -l | tr -d ' ')
133
+ log "Found $count merged branch(es): $(echo "$merged" | tr '\n' ' ')"
134
+ while IFS= read -r b; do
135
+ [ -z "$b" ] && continue
136
+ delete_branch "$b"
137
+ done <<< "$merged"
138
+ }
139
+
140
+ resync_work_branch() {
141
+ local work="$1" target="$2"
142
+ { [ -z "$work" ] || [ "$REMOTE_EXISTS" = false ]; } && return 0
143
+ log "Resyncing $work <- $target..."
144
+ local saved; saved=$(git branch --show-current)
145
+ if git rev-parse --verify "refs/heads/$work" >/dev/null 2>&1; then
146
+ run_cmd git checkout "$work" || { log "Warning: could not checkout $work; skipping resync"; return 0; }
147
+ elif git rev-parse --verify "refs/remotes/origin/$work" >/dev/null 2>&1; then
148
+ log " Creating local branch $work from origin/$work"
149
+ run_cmd git checkout -b "$work" "origin/$work" || { log "Warning: could not create $work; skipping resync"; return 0; }
150
+ else
151
+ log " Warning: branch $work does not exist locally or on origin; skipping resync"
152
+ return 0
153
+ fi
154
+ if ! run_cmd git pull origin "$target" --ff-only 2>/dev/null; then
155
+ if [ "$AUTO_MODE" = true ]; then
156
+ echo "Warning: resync $work <- $target needs non-ff merge. Run manually."
157
+ else
158
+ read -r -p "Fast-forward failed. Attempt merge? [y/N]: " c
159
+ if [ "$c" = "y" ] || [ "$c" = "Y" ]; then
160
+ run_cmd git merge "origin/$target" 2>/dev/null \
161
+ || echo "Error: conflict. Resolve then: git push origin $work"
121
162
  fi
122
163
  fi
164
+ else
165
+ if ! run_cmd git push origin "$work" 2>/dev/null; then
166
+ log "Warning: push $work failed (network?)"
167
+ fi
123
168
  fi
124
- done <<< "$MERGED_BRANCHES"
125
-
126
- # --- Post-cleanup actions ---
127
- if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$REMOTE_EXISTS" = true ]; then
128
- [ "$AUTO_MODE" = false ] && echo "Pulling latest from $MAIN_BRANCH..."
129
- git pull origin "$MAIN_BRANCH" 2>/dev/null || echo "Failed to pull from $MAIN_BRANCH (network issue or conflicts)"
130
- fi
131
-
132
- # Feature branch creation prompt (only for feature-branches strategy, not in auto mode)
133
- if [ "$AUTO_MODE" = false ] && [ "$BRANCHING" = "feature-branches" ] && [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ]; then
134
- read -r -p "Create new feature branch? [y/N]: " create_branch
135
- if [ "$create_branch" = "y" ] || [ "$create_branch" = "Y" ]; then
136
- read -r -p "Branch name (default: feat/$(date +%s)): " branch_name
137
- branch_name="${branch_name:-feat/$(date +%s)}"
138
- git checkout -b "$branch_name" || echo "Failed to create branch $branch_name"
169
+ if [ "$saved" != "$work" ]; then
170
+ run_cmd git checkout "$saved" 2>/dev/null || log "Warning: could not return to branch $saved"
139
171
  fi
140
- fi
141
-
142
- [ "$AUTO_MODE" = false ] && echo "Cleanup complete!"
172
+ }
173
+
174
+ prompt_new_feature_branch() {
175
+ [ "$AUTO_MODE" = true ] && return 0
176
+ [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] || return 0
177
+ read -r -p "Create new feature branch? [y/N]: " c
178
+ { [ "$c" = "y" ] || [ "$c" = "Y" ]; } || return 0
179
+ read -r -p "Branch name (default: feat/$(date +%s)): " name
180
+ name="${name:-feat/$(date +%s)}"
181
+ run_cmd git checkout -b "$name" || log "Failed to create branch $name"
182
+ }
183
+
184
+ # --- 7. STRATEGY DISPATCH ---
185
+
186
+ strategy_direct_main() {
187
+ delete_merged_branches
188
+ if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$REMOTE_EXISTS" = true ]; then
189
+ log "Pulling latest from $MAIN_BRANCH..."
190
+ run_cmd git pull origin "$MAIN_BRANCH" 2>/dev/null \
191
+ || log "Failed to pull from $MAIN_BRANCH"
192
+ fi
193
+ }
194
+
195
+ strategy_feature_branches() {
196
+ delete_merged_branches
197
+ if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$REMOTE_EXISTS" = true ]; then
198
+ log "Pulling latest from $MAIN_BRANCH..."
199
+ run_cmd git pull origin "$MAIN_BRANCH" 2>/dev/null \
200
+ || log "Failed to pull from $MAIN_BRANCH"
201
+ fi
202
+ prompt_new_feature_branch
203
+ }
204
+
205
+ strategy_dev_branch() {
206
+ delete_merged_branches "$MAIN_BRANCH"
207
+ delete_merged_branches "$WORK_BRANCH"
208
+ resync_work_branch "$WORK_BRANCH" "$MAIN_BRANCH"
209
+ }
210
+
211
+ strategy_gitflow() {
212
+ # Step 1: feat/* merged into develop → delete
213
+ if git rev-parse --verify "$WORK_BRANCH" >/dev/null 2>&1; then
214
+ log "Cleaning branches merged into $WORK_BRANCH..."
215
+ delete_merged_branches "$WORK_BRANCH"
216
+ fi
217
+ # Step 2: release/* and hotfix/* merged into main → delete
218
+ log "Cleaning branches merged into $MAIN_BRANCH..."
219
+ delete_merged_branches "$MAIN_BRANCH"
220
+ # Step 3: resync develop ← main
221
+ resync_work_branch "$WORK_BRANCH" "$MAIN_BRANCH"
222
+ }
223
+
224
+ case "$BRANCHING" in
225
+ direct-main) strategy_direct_main ;;
226
+ feature-branches) strategy_feature_branches ;;
227
+ dev-branch) strategy_dev_branch ;;
228
+ gitflow) strategy_gitflow ;;
229
+ *) echo "Unknown branching strategy: $BRANCHING" >&2; exit 1 ;;
230
+ esac
231
+
232
+ log "Cleanup complete!"
143
233
  exit 0
@@ -0,0 +1,212 @@
1
+ #!/bin/bash
2
+ # restore.sh — CortexHawk snapshot restoration
3
+ # Sourced by install.sh when --restore is used
4
+ # Uses shared functions: get_version, compute_checksum, copy_all_components, write_manifest
5
+ # Uses globals: GLOBAL, PROFILE, PROFILE_FILE, SCRIPT_DIR
6
+
7
+ do_restore() {
8
+ local snap_file="$1"
9
+ local archive_tmp=""
10
+ local portable_files=""
11
+
12
+ if [ -z "$snap_file" ] || [ ! -f "$snap_file" ]; then
13
+ echo "Error: snapshot file not found: $snap_file"
14
+ exit 1
15
+ fi
16
+
17
+ # Handle portable archive (.tar.gz)
18
+ if [[ "$snap_file" == *.tar.gz ]]; then
19
+ archive_tmp=$(mktemp -d)
20
+ # Validate archive paths before extraction (reject ../ and absolute paths)
21
+ while IFS= read -r entry; do
22
+ case "$entry" in
23
+ /*|*../*|*/..*|..) echo "Error: archive contains unsafe path: $entry"; rm -rf "$archive_tmp"; exit 1 ;;
24
+ esac
25
+ done < <(tar -tzf "$snap_file" 2>/dev/null || { echo "Error: invalid archive"; rm -rf "$archive_tmp"; exit 1; })
26
+ tar -xzf "$snap_file" -C "$archive_tmp"
27
+ snap_file="$archive_tmp/snapshot.json"
28
+ portable_files="$archive_tmp/files"
29
+ if [ ! -f "$snap_file" ]; then
30
+ echo "Error: invalid archive — snapshot.json not found"
31
+ rm -rf "$archive_tmp"
32
+ exit 1
33
+ fi
34
+ echo "Extracting portable archive..."
35
+ fi
36
+
37
+ # Extract metadata from snapshot
38
+ local snap_version
39
+ snap_version=$(grep '"cortexhawk_version"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
40
+ local snap_profile
41
+ snap_profile=$(grep '"profile"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
42
+ local snap_source
43
+ snap_source=$(grep '"source"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
44
+ local snap_source_url
45
+ snap_source_url=$(grep '"source_url"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
46
+ local snap_source_path
47
+ snap_source_path=$(grep '"source_path"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
48
+
49
+ echo "CortexHawk Restore"
50
+ echo "====================="
51
+ echo " CortexHawk version: $snap_version"
52
+ echo " Profile: $snap_profile"
53
+ echo " Source: $snap_source"
54
+ echo ""
55
+
56
+ # Determine CortexHawk source
57
+ local restore_source="$SCRIPT_DIR"
58
+ if [ "$snap_source" = "git" ] && [ -n "$snap_source_path" ] && [ -d "$snap_source_path" ]; then
59
+ restore_source="$snap_source_path"
60
+ fi
61
+ if [ ! -d "$restore_source/agents" ]; then
62
+ echo "Error: CortexHawk source not found at $restore_source"
63
+ echo "Ensure the CortexHawk repo is available or set CORTEXHAWK_REPO"
64
+ exit 1
65
+ fi
66
+
67
+ # Set profile for reinstall
68
+ if [ -n "$snap_profile" ] && [ "$snap_profile" != "all" ]; then
69
+ PROFILE="$snap_profile"
70
+ PROFILE_FILE="$restore_source/profiles/${snap_profile}.json"
71
+ if [ ! -f "$PROFILE_FILE" ]; then
72
+ echo " Warning: profile '$snap_profile' not found — installing all skills"
73
+ PROFILE=""
74
+ PROFILE_FILE=""
75
+ fi
76
+ fi
77
+
78
+ # Determine target
79
+ if [ "$GLOBAL" = true ]; then
80
+ TARGET="$HOME/.claude"
81
+ else
82
+ TARGET="$(pwd)/.claude"
83
+ fi
84
+
85
+ # Save the original SCRIPT_DIR, use snapshot source
86
+ local orig_script_dir="$SCRIPT_DIR"
87
+ SCRIPT_DIR="$restore_source"
88
+
89
+ # Warn if restore source version differs from snapshot
90
+ local current_version
91
+ current_version=$(get_version)
92
+ if [ "$snap_version" != "$current_version" ]; then
93
+ echo " Warning: snapshot is v$snap_version but restore source is v$current_version"
94
+ echo " Some file checksums may not match"
95
+ echo ""
96
+ fi
97
+
98
+ # Reinstall using the standard flow
99
+ echo "Reinstalling CortexHawk components..."
100
+
101
+ # Use portable archive files if available, otherwise use source repo
102
+ if [ -n "$portable_files" ] && [ -d "$portable_files" ]; then
103
+ echo " Using files from portable archive..."
104
+ copy_all_components "$portable_files" "$TARGET" ""
105
+ else
106
+ copy_all_components "$SCRIPT_DIR" "$TARGET" "$PROFILE"
107
+ fi
108
+
109
+ # Restore settings.json from snapshot
110
+ if command -v python3 >/dev/null 2>&1; then
111
+ python3 -c "
112
+ import json, sys
113
+ with open(sys.argv[1]) as f:
114
+ snap = json.load(f)
115
+ settings = snap.get('settings')
116
+ if settings is not None:
117
+ with open(sys.argv[2], 'w') as f:
118
+ json.dump(settings, f, indent=2)
119
+ f.write('\n')
120
+ print(' Restored settings.json')
121
+ " "$snap_file" "$TARGET/settings.json"
122
+ else
123
+ echo " Warning: python3 not found — settings.json not restored from snapshot"
124
+ fi
125
+
126
+ # Restore git-workflow.conf from snapshot
127
+ local branching commit_conv pr_pref auto_push
128
+ branching=$(grep '"BRANCHING"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
129
+ commit_conv=$(grep '"COMMIT_CONVENTION"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
130
+ pr_pref=$(grep '"PR_PREFERENCE"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
131
+ auto_push=$(grep '"AUTO_PUSH"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
132
+
133
+ if [ -n "$branching" ] || [ -n "$commit_conv" ] || [ -n "$pr_pref" ] || [ -n "$auto_push" ]; then
134
+ {
135
+ echo "BRANCHING=$branching"
136
+ echo "COMMIT_CONVENTION=$commit_conv"
137
+ echo "PR_PREFERENCE=$pr_pref"
138
+ echo "AUTO_PUSH=$auto_push"
139
+ } > "$TARGET/git-workflow.conf"
140
+ echo " Restored git-workflow.conf (from git_workflow keys)"
141
+ fi
142
+
143
+ # Restore file_contents (snapshot v2) — overwrites git-workflow.conf if present
144
+ if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
145
+ python3 -c "
146
+ import json, base64, sys, os
147
+ snap_file, target_dir = sys.argv[1], sys.argv[2]
148
+ with open(snap_file) as f:
149
+ snap = json.load(f)
150
+ contents = snap.get('file_contents', {})
151
+ for filename, b64data in contents.items():
152
+ try:
153
+ # Reject path traversal and absolute paths
154
+ if '..' in filename or filename.startswith('/'):
155
+ print(f' Warning: skipping unsafe path: {filename}', file=sys.stderr)
156
+ continue
157
+ data = base64.b64decode(b64data).decode('utf-8')
158
+ if filename == 'CLAUDE.md':
159
+ target_path = os.path.dirname(target_dir) + '/CLAUDE.md'
160
+ else:
161
+ target_path = os.path.normpath(target_dir + '/' + filename)
162
+ if not target_path.startswith(target_dir + os.sep):
163
+ print(f' Warning: skipping path outside target: {filename}', file=sys.stderr)
164
+ continue
165
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
166
+ with open(target_path, 'w') as f:
167
+ f.write(data)
168
+ print(f' Restored {filename} (from file_contents)')
169
+ except Exception as e:
170
+ print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
171
+ " "$snap_file" "$TARGET"
172
+ fi
173
+
174
+ # Write new manifest
175
+ write_manifest "$TARGET" "$PROFILE" "claude" false
176
+
177
+ # Verify checksums against snapshot
178
+ local verified=0 mismatched=0 missing=0
179
+ while IFS= read -r line; do
180
+ local file_relpath
181
+ file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
182
+ local expected_checksum
183
+ expected_checksum=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
184
+ [ -z "$file_relpath" ] || [ -z "$expected_checksum" ] && continue
185
+
186
+ local target_file="$TARGET/$file_relpath"
187
+ if [ ! -f "$target_file" ]; then
188
+ missing=$((missing + 1))
189
+ else
190
+ local actual_checksum
191
+ actual_checksum=$(compute_checksum "$target_file")
192
+ if [ "$actual_checksum" = "$expected_checksum" ]; then
193
+ verified=$((verified + 1))
194
+ else
195
+ mismatched=$((mismatched + 1))
196
+ fi
197
+ fi
198
+ done < <(grep '"sha256:' "$snap_file")
199
+
200
+ SCRIPT_DIR="$orig_script_dir"
201
+
202
+ echo ""
203
+ echo "Restore complete"
204
+ echo " Verified: $verified files match snapshot checksums"
205
+ [ "$mismatched" -gt 0 ] && echo " Mismatched: $mismatched files differ (source version may differ from snapshot)"
206
+ [ "$missing" -gt 0 ] && echo " Missing: $missing files not found in source"
207
+ echo ""
208
+ echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
209
+
210
+ # Cleanup temp dir from portable archive
211
+ [ -n "$archive_tmp" ] && rm -rf "$archive_tmp"
212
+ }