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.
- package/CHANGELOG.md +21 -1
- package/README.md +25 -7
- package/commands/cleanup.md +1 -0
- package/cortexhawk +7 -1
- package/hooks/branch-guard.sh +1 -2
- package/hooks/codex-dispatcher.sh +3 -0
- package/install.sh +55 -934
- package/mcp/context7.json +1 -1
- package/mcp/github.json +1 -1
- package/mcp/puppeteer.json +1 -1
- package/mcp/sequential-thinking.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.sh +164 -0
- package/scripts/install-claude.sh +179 -0
- package/scripts/post-merge-cleanup.sh +170 -80
- package/scripts/restore.sh +212 -0
- package/scripts/snapshot.sh +163 -0
- package/scripts/update.sh +280 -0
- package/templates/AGENT.md +19 -0
- package/templates/CLAUDE.md.template +41 -0
- package/templates/COMMAND.md +14 -0
- package/templates/ORCHESTRATION.md +79 -0
- package/templates/PERSONA.md +17 -0
- package/templates/SKILL.md +17 -0
- package/templates/github/PULL_REQUEST_TEMPLATE.md +26 -0
- package/templates/github/gitmessage +10 -0
|
@@ -1,41 +1,47 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# post-merge-cleanup.sh —
|
|
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,
|
|
5
|
+
# Args: --auto (silent mode, no prompts), --dry-run (preview without executing)
|
|
5
6
|
|
|
6
7
|
set -e
|
|
7
8
|
|
|
8
|
-
#
|
|
9
|
+
# --- 1. FLAGS ---
|
|
9
10
|
AUTO_MODE=false
|
|
10
|
-
|
|
11
|
-
[ ! -t 0 ] && AUTO_MODE=true
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
# ---
|
|
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
|
-
|
|
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
|
-
# ---
|
|
102
|
-
while IFS= read -r branch; do
|
|
103
|
-
[ -z "$branch" ] && continue
|
|
102
|
+
# --- 6. HELPERS ---
|
|
104
103
|
|
|
105
|
-
|
|
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 ||
|
|
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]: "
|
|
110
|
-
|
|
111
|
-
git branch -d "$branch" ||
|
|
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
|
|
119
|
-
|
|
120
|
-
git push origin --delete "$branch" 2>/dev/null ||
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|