cortexhawk 3.2.0 → 3.3.1

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.
@@ -0,0 +1,233 @@
1
+ #!/bin/bash
2
+ # post-merge-cleanup.sh — Unified post-merge cleanup for all git workflow strategies
3
+ # Strategies: direct-main, feature-branches, dev-branch, gitflow
4
+ # Called by /cleanup command and post-merge hook
5
+ # Args: --auto (silent mode, no prompts), --dry-run (preview without executing)
6
+
7
+ set -e
8
+
9
+ # --- 1. FLAGS ---
10
+ AUTO_MODE=false
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
19
+
20
+ # --- 2. GIT VALIDATION ---
21
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
22
+ echo "Error: not a git repository"
23
+ exit 1
24
+ fi
25
+
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) ---
33
+ BRANCHING="direct-main"
34
+ MAIN_BRANCH="main"
35
+ WORK_BRANCH=""
36
+
37
+ # Layer 1: .claude/git-workflow.conf (safe parsing, no source)
38
+ if [ -f ".claude/git-workflow.conf" ]; then
39
+ BRANCHING=$(grep -E '^BRANCHING=' ".claude/git-workflow.conf" | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'")
40
+ WORK_BRANCH=$(grep -E '^WORK_BRANCH=' ".claude/git-workflow.conf" | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'")
41
+ case "$BRANCHING" in
42
+ dev-branch) MAIN_BRANCH="main" ;;
43
+ gitflow) MAIN_BRANCH="main"; WORK_BRANCH="${WORK_BRANCH:-develop}" ;;
44
+ feature-branches|direct-main) MAIN_BRANCH="main" ;;
45
+ esac
46
+ fi
47
+
48
+ # Layer 2: Fallback to CLAUDE.md
49
+ if [ ! -f ".claude/git-workflow.conf" ] && [ -f "CLAUDE.md" ]; then
50
+ if grep -q "^## Git Workflow" "CLAUDE.md"; then
51
+ DETECTED=$(grep -i "branching:" "CLAUDE.md" | head -1 | sed 's/.*: //' | awk '{print $1}')
52
+ case "$DETECTED" in
53
+ dev-branch)
54
+ BRANCHING="dev-branch"
55
+ WORK_BRANCH=$(grep -i "working branch:" "CLAUDE.md" | head -1 | sed 's/.*: //' | tr -d ')')
56
+ MAIN_BRANCH="main"
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" ;;
61
+ esac
62
+ fi
63
+ fi
64
+
65
+ # Layer 3: verify MAIN_BRANCH exists (main vs master fallback)
66
+ if ! git rev-parse --verify "$MAIN_BRANCH" >/dev/null 2>&1; then
67
+ if git rev-parse --verify main >/dev/null 2>&1; then
68
+ MAIN_BRANCH="main"
69
+ elif git rev-parse --verify master >/dev/null 2>&1; then
70
+ MAIN_BRANCH="master"
71
+ else
72
+ echo "Error: branch $MAIN_BRANCH does not exist"; exit 1
73
+ fi
74
+ fi
75
+
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
+ }
92
+
93
+ # --- 5. REMOTE CHECK ---
94
+ REMOTE_EXISTS=true
95
+ if ! git remote get-url origin >/dev/null 2>&1; then
96
+ REMOTE_EXISTS=false
97
+ log "Warning: no remote 'origin' configured, skipping remote operations"
98
+ fi
99
+ CURRENT_BRANCH=$(git branch --show-current)
100
+ log "Strategy: $BRANCHING | main: $MAIN_BRANCH | work: ${WORK_BRANCH:-(none)}"
101
+
102
+ # --- 6. HELPERS ---
103
+
104
+ delete_branch() {
105
+ local branch="$1"
106
+ is_protected "$branch" && { log " Protected, skipping: $branch"; return 0; }
107
+ if [ "$AUTO_MODE" = true ]; then
108
+ run_cmd git branch -d "$branch" 2>/dev/null || log " Could not delete $branch"
109
+ else
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
+ fi
114
+ if [ "$AUTO_MODE" = false ] && [ "$REMOTE_EXISTS" = true ]; then
115
+ if git rev-parse --verify "refs/remotes/origin/$branch" >/dev/null 2>&1; then
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"
162
+ fi
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
168
+ fi
169
+ if [ "$saved" != "$work" ]; then
170
+ run_cmd git checkout "$saved" 2>/dev/null || log "Warning: could not return to branch $saved"
171
+ fi
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!"
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
+ }
@@ -0,0 +1,163 @@
1
+ #!/bin/bash
2
+ # snapshot.sh — CortexHawk snapshot creation and rotation
3
+ # Sourced by install.sh when --snapshot is used
4
+ # Uses shared functions: sed_inplace, compute_checksum (via manifest)
5
+ # Uses globals: GLOBAL, TARGET, PORTABLE_MODE, PROFILE_FILE, MAX_SNAPSHOTS
6
+
7
+ # --- rotate_snapshots() ---
8
+ # Keeps only the N most recent snapshots, deletes the rest
9
+ rotate_snapshots() {
10
+ local snap_dir="$1"
11
+ local snaps
12
+ snaps=$(find "$snap_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
13
+ [ -z "$snaps" ] && return 0
14
+ local count
15
+ count=$(echo "$snaps" | wc -l | tr -d ' ')
16
+ if [ "$count" -gt "$MAX_SNAPSHOTS" ]; then
17
+ local to_delete=$((count - MAX_SNAPSHOTS))
18
+ ls -1t "$snap_dir"/*.json | tail -n "$to_delete" | while read -r old_snap; do
19
+ rm -f "$old_snap"
20
+ done
21
+ echo " Rotated: removed $to_delete old snapshot(s), keeping $MAX_SNAPSHOTS"
22
+ fi
23
+ }
24
+
25
+ # --- do_snapshot() ---
26
+ do_snapshot() {
27
+ if [ "$GLOBAL" = true ]; then
28
+ TARGET="$HOME/.claude"
29
+ else
30
+ TARGET="$(pwd)/.claude"
31
+ fi
32
+
33
+ local manifest="$TARGET/.cortexhawk-manifest"
34
+ if [ ! -f "$manifest" ]; then
35
+ echo "Error: no CortexHawk manifest found at $manifest"
36
+ echo "Run install.sh first to create an installation"
37
+ exit 1
38
+ fi
39
+
40
+ # Read manifest metadata
41
+ local version
42
+ version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
43
+ local profile
44
+ profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
45
+ local source_type
46
+ source_type=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
47
+ local source_url
48
+ source_url=$(grep '"source_url"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
49
+ local source_path
50
+ source_path=$(grep '"source_path"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
51
+
52
+ # Read settings.json
53
+ local settings_json="null"
54
+ if [ -f "$TARGET/settings.json" ]; then
55
+ settings_json=$(cat "$TARGET/settings.json")
56
+ fi
57
+
58
+ # Read git-workflow.conf
59
+ local git_branching="" git_commit="" git_pr="" git_push=""
60
+ if [ -f "$TARGET/git-workflow.conf" ]; then
61
+ git_branching=$(grep '^BRANCHING=' "$TARGET/git-workflow.conf" | cut -d= -f2)
62
+ git_commit=$(grep '^COMMIT_CONVENTION=' "$TARGET/git-workflow.conf" | cut -d= -f2)
63
+ git_pr=$(grep '^PR_PREFERENCE=' "$TARGET/git-workflow.conf" | cut -d= -f2)
64
+ git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
65
+ fi
66
+
67
+ # Read custom profile if applicable (use PROFILE_FILE from current run, never glob /tmp/)
68
+ local profile_def="null"
69
+ if [ -n "$PROFILE_FILE" ] && [ -f "$PROFILE_FILE" ]; then
70
+ profile_def=$(cat "$PROFILE_FILE")
71
+ fi
72
+
73
+ # Build files checksums from manifest
74
+ local files_json
75
+ files_json=$(sed -n '/"files"/,/^ }/p' "$manifest" | sed '1d;$d')
76
+
77
+ # Collect file contents (base64 encoded for binary safety)
78
+ local git_workflow_content="" claude_md_content=""
79
+ if [ -f "$TARGET/git-workflow.conf" ]; then
80
+ git_workflow_content=$(base64 < "$TARGET/git-workflow.conf" | tr -d '\n')
81
+ fi
82
+ if [ -f "$TARGET/../CLAUDE.md" ]; then
83
+ claude_md_content=$(base64 < "$TARGET/../CLAUDE.md" | tr -d '\n')
84
+ fi
85
+
86
+ # Generate snapshot
87
+ local now
88
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
89
+ local snap_name
90
+ snap_name=$(date -u +"%Y-%m-%d-%H%M%S")
91
+ local snap_dir="$TARGET/.cortexhawk-snapshots"
92
+ local snap_file="$snap_dir/${snap_name}.json"
93
+
94
+ mkdir -p "$snap_dir"
95
+
96
+ # Write snapshot JSON
97
+ printf '{\n' > "$snap_file"
98
+ printf ' "snapshot_version": "2",\n' >> "$snap_file"
99
+ printf ' "snapshot_date": "%s",\n' "$now" >> "$snap_file"
100
+ printf ' "cortexhawk_version": "%s",\n' "$version" >> "$snap_file"
101
+ printf ' "target": "claude",\n' >> "$snap_file"
102
+ printf ' "profile": "%s",\n' "$profile" >> "$snap_file"
103
+ printf ' "profile_definition": %s,\n' "$profile_def" >> "$snap_file"
104
+ printf ' "source": "%s",\n' "$source_type" >> "$snap_file"
105
+ printf ' "source_url": "%s",\n' "$source_url" >> "$snap_file"
106
+ printf ' "source_path": "%s",\n' "$source_path" >> "$snap_file"
107
+ printf ' "settings": %s,\n' "$settings_json" >> "$snap_file"
108
+ printf ' "git_workflow": {\n' >> "$snap_file"
109
+ printf ' "BRANCHING": "%s",\n' "$git_branching" >> "$snap_file"
110
+ printf ' "COMMIT_CONVENTION": "%s",\n' "$git_commit" >> "$snap_file"
111
+ printf ' "PR_PREFERENCE": "%s",\n' "$git_pr" >> "$snap_file"
112
+ printf ' "AUTO_PUSH": "%s"\n' "$git_push" >> "$snap_file"
113
+ printf ' },\n' >> "$snap_file"
114
+ printf ' "files": {\n' >> "$snap_file"
115
+ printf '%s\n' "$files_json" >> "$snap_file"
116
+ printf ' },\n' >> "$snap_file"
117
+ printf ' "file_contents": {\n' >> "$snap_file"
118
+ [ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
119
+ [ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
120
+ # Remove trailing comma from last entry
121
+ sed_inplace '$ s/,$//' "$snap_file"
122
+ printf ' }\n' >> "$snap_file"
123
+ printf '}\n' >> "$snap_file"
124
+
125
+ echo "CortexHawk Snapshot"
126
+ echo "====================="
127
+ echo " Version: $version"
128
+ echo " Profile: $profile"
129
+ echo " Target: $TARGET"
130
+ echo " Saved to: $snap_file"
131
+
132
+ # Create portable archive if requested
133
+ if [ "$PORTABLE_MODE" = true ]; then
134
+ local archive_name="${snap_name}.tar.gz"
135
+ local archive_path="$snap_dir/$archive_name"
136
+ local tmp_dir
137
+ tmp_dir=$(mktemp -d)
138
+
139
+ # Copy snapshot and files to temp structure
140
+ cp "$snap_file" "$tmp_dir/snapshot.json"
141
+ mkdir -p "$tmp_dir/files"
142
+ cp -r "$TARGET/agents" "$tmp_dir/files/" 2>/dev/null || true
143
+ cp -r "$TARGET/commands" "$tmp_dir/files/" 2>/dev/null || true
144
+ cp -r "$TARGET/skills" "$tmp_dir/files/" 2>/dev/null || true
145
+ cp -r "$TARGET/hooks" "$tmp_dir/files/" 2>/dev/null || true
146
+ cp -r "$TARGET/modes" "$tmp_dir/files/" 2>/dev/null || true
147
+ cp -r "$TARGET/mcp" "$tmp_dir/files/" 2>/dev/null || true
148
+ cp "$TARGET/settings.json" "$tmp_dir/files/" 2>/dev/null || true
149
+ cp "$TARGET/git-workflow.conf" "$tmp_dir/files/" 2>/dev/null || true
150
+
151
+ # Create archive
152
+ tar -czf "$archive_path" -C "$tmp_dir" .
153
+ rm -rf "$tmp_dir"
154
+
155
+ echo " Archive: $archive_path"
156
+ echo ""
157
+ echo "Restore with: install.sh --restore $archive_path"
158
+ else
159
+ rotate_snapshots "$snap_dir"
160
+ echo ""
161
+ echo "Restore with: install.sh --restore $snap_file"
162
+ fi
163
+ }