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.
- package/.cortexhawk-lint.yml.example +21 -0
- package/.gitmessage +10 -0
- package/CHANGELOG.md +45 -0
- package/CLAUDE.md +12 -4
- package/agents/git-manager.md +6 -2
- package/commands/backlog.md +1 -1
- package/commands/cleanup.md +37 -0
- package/commands/review-pr.md +31 -0
- package/commands/ship.md +1 -0
- package/commands/task.md +1 -1
- package/cortexhawk +9 -3
- package/hooks/branch-guard.sh +8 -1
- package/hooks/codex-dispatcher.sh +3 -0
- package/hooks/compose.yml +6 -0
- package/hooks/file-guard.sh +4 -0
- package/hooks/hooks.json +6 -0
- package/hooks/lint-guard.sh +46 -0
- package/hooks/post-merge.sh +12 -0
- package/hooks/session-start.sh +1 -1
- package/install.sh +159 -962
- package/mcp/README.md +36 -0
- package/mcp/context7.json +1 -1
- package/mcp/github.json +11 -0
- package/mcp/puppeteer.json +1 -1
- package/mcp/sequential-thinking.json +1 -1
- package/package.json +1 -1
- package/profiles/api.json +2 -1
- package/profiles/fullstack.json +2 -1
- package/scripts/autodetect-profile.sh +1 -1
- package/scripts/doctor.sh +164 -0
- package/scripts/install-claude.sh +179 -0
- package/scripts/interactive-init.sh +3 -2
- package/scripts/lint-guard-runner.sh +132 -0
- package/scripts/post-merge-cleanup.sh +233 -0
- package/scripts/restore.sh +212 -0
- package/scripts/snapshot.sh +163 -0
- package/scripts/update.sh +280 -0
- package/settings.json +12 -1
package/install.sh
CHANGED
|
@@ -7,6 +7,16 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
7
7
|
if [ -f ".env" ]; then
|
|
8
8
|
while IFS='=' read -r key value; do
|
|
9
9
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
10
|
+
# Validate key format: uppercase letters, digits, underscore only (POSIX env var names)
|
|
11
|
+
[[ "$key" =~ ^[A-Z_][A-Z0-9_]{0,63}$ ]] || continue
|
|
12
|
+
# Block dangerous variable names that could hijack script/shell behavior
|
|
13
|
+
case "$key" in
|
|
14
|
+
PATH|LD_PRELOAD|LD_LIBRARY_PATH|LD_AUDIT|LD_DEBUG|BASH_ENV|BASH_FUNC_*|\
|
|
15
|
+
SCRIPT_DIR|HOME|SHELL|IFS|CDPATH|ENV|PYTHONPATH|PYTHONSTARTUP|\
|
|
16
|
+
GIT_SSH|GIT_SSH_COMMAND|GIT_PROXY_COMMAND|GIT_EXEC_PATH|\
|
|
17
|
+
NPM_CONFIG_*|NODE_OPTIONS|RUBYLIB|RUBYOPT|PERL5LIB|PERL5OPT)
|
|
18
|
+
continue ;;
|
|
19
|
+
esac
|
|
10
20
|
# Strip double quotes
|
|
11
21
|
value="${value%\"}" && value="${value#\"}"
|
|
12
22
|
# Strip single quotes
|
|
@@ -24,7 +34,7 @@ green() { printf "\033[32m%s\033[0m\n" "$1"; }
|
|
|
24
34
|
yellow() { printf "\033[33m%s\033[0m\n" "$1"; }
|
|
25
35
|
|
|
26
36
|
get_version() {
|
|
27
|
-
grep -m1 '## \[' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
|
|
37
|
+
grep -m1 '## \[[0-9]' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
# Portable sed -i (GNU vs BSD)
|
|
@@ -161,7 +171,26 @@ STATS_MODE=false
|
|
|
161
171
|
PUBLISH_SKILL_PATH=""
|
|
162
172
|
CHECK_UPDATE_MODE=false
|
|
163
173
|
DEMO_MODE=false
|
|
174
|
+
TRUST_SKILL=false
|
|
164
175
|
MAX_SNAPSHOTS=10
|
|
176
|
+
POST_MERGE_HOOK_MODE=false
|
|
177
|
+
|
|
178
|
+
# === Component Registry ===
|
|
179
|
+
# Single source of truth for all CortexHawk components.
|
|
180
|
+
# Format: "name:executable"
|
|
181
|
+
# - executable = "yes" runs chmod +x *.sh after copy (hooks, scripts)
|
|
182
|
+
# - executable = "no" for markdown-only components (agents, commands, modes)
|
|
183
|
+
# - skills handled specially via copy_skills()/sync_skills_update() for profile filtering
|
|
184
|
+
# Used by: copy_all_components(), sync_all_components(), count_component_files()
|
|
185
|
+
COMPONENTS=(
|
|
186
|
+
"agents:no"
|
|
187
|
+
"commands:no"
|
|
188
|
+
"skills:no"
|
|
189
|
+
"hooks:yes"
|
|
190
|
+
"modes:no"
|
|
191
|
+
"mcp:no"
|
|
192
|
+
"scripts:yes"
|
|
193
|
+
)
|
|
165
194
|
|
|
166
195
|
while [ $# -gt 0 ]; do
|
|
167
196
|
case "$1" in
|
|
@@ -300,6 +329,10 @@ while [ $# -gt 0 ]; do
|
|
|
300
329
|
DEMO_MODE=true
|
|
301
330
|
shift
|
|
302
331
|
;;
|
|
332
|
+
--trust)
|
|
333
|
+
TRUST_SKILL=true
|
|
334
|
+
shift
|
|
335
|
+
;;
|
|
303
336
|
--publish-skill)
|
|
304
337
|
PUBLISH_SKILL_PATH="$2"
|
|
305
338
|
if [ -z "$PUBLISH_SKILL_PATH" ]; then
|
|
@@ -352,6 +385,10 @@ while [ $# -gt 0 ]; do
|
|
|
352
385
|
fi
|
|
353
386
|
shift 2
|
|
354
387
|
;;
|
|
388
|
+
--post-merge-hook)
|
|
389
|
+
POST_MERGE_HOOK_MODE=true
|
|
390
|
+
shift
|
|
391
|
+
;;
|
|
355
392
|
--version|-v)
|
|
356
393
|
echo "CortexHawk $(get_version)"
|
|
357
394
|
exit 0
|
|
@@ -455,7 +492,7 @@ if [ -n "$PACK_NAME" ]; then
|
|
|
455
492
|
exit 1
|
|
456
493
|
fi
|
|
457
494
|
_skills_csv=$(echo "$_row" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
458
|
-
_description=$(echo "$_row" | awk -F'|' '{print $4}' | sed 's/^ *//;s/ *$//')
|
|
495
|
+
_description=$(echo "$_row" | awk -F'|' '{print $4}' | sed 's/^ *//;s/ *$//' | sed 's/"/\\"/g')
|
|
459
496
|
_tmp_profile=$(mktemp /tmp/cortexhawk-pack-XXXXXX.json)
|
|
460
497
|
{
|
|
461
498
|
echo '{'
|
|
@@ -505,11 +542,12 @@ detect_installed_clis() {
|
|
|
505
542
|
copy_skills() {
|
|
506
543
|
local target_dir="$1"
|
|
507
544
|
local profile="$2"
|
|
545
|
+
local source_dir="${3:-$SCRIPT_DIR}"
|
|
508
546
|
if [ -z "$profile" ]; then
|
|
509
|
-
cp -r "$
|
|
547
|
+
cp -r "$source_dir/skills/"* "$target_dir/skills/" 2>/dev/null || true
|
|
510
548
|
else
|
|
511
549
|
grep '"[a-z]' "$PROFILE_FILE" | sed 's/.*"\([a-z][^"]*\)".*/\1/' | grep '/' | while read -r skill; do
|
|
512
|
-
local src="$
|
|
550
|
+
local src="$source_dir/skills/$skill"
|
|
513
551
|
local dest="$target_dir/skills/$skill"
|
|
514
552
|
if [ ! -d "$src" ]; then
|
|
515
553
|
yellow " Warning: skill '$skill' not found in source — skipping"
|
|
@@ -574,6 +612,9 @@ convert_modes_to_skills() {
|
|
|
574
612
|
done
|
|
575
613
|
}
|
|
576
614
|
|
|
615
|
+
# NOTE: profiles/*.json contain an "mcp" field listing recommended servers, but install.sh
|
|
616
|
+
# currently installs all mcp/*.json regardless of profile. Per-profile MCP filtering is planned
|
|
617
|
+
# for a future phase of #137 (install.sh modularization).
|
|
577
618
|
# merge_mcp_json(output_file) — merges mcp/*.json into single JSON file
|
|
578
619
|
merge_mcp_json() {
|
|
579
620
|
local output_file="$1"
|
|
@@ -582,7 +623,7 @@ merge_mcp_json() {
|
|
|
582
623
|
[ -f "$mcp_file" ] || continue
|
|
583
624
|
local server_name command_val args_val entry
|
|
584
625
|
server_name=$(basename "$mcp_file" .json)
|
|
585
|
-
command_val=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
|
626
|
+
command_val=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | sed 's/"/\\"/g')
|
|
586
627
|
args_val=$(grep '"args"' "$mcp_file" | sed 's/.*"args"[[:space:]]*:[[:space:]]*\(\[.*\]\).*/\1/')
|
|
587
628
|
entry=" \"$server_name\": {\n \"command\": \"$command_val\",\n \"args\": $args_val\n }"
|
|
588
629
|
if [ -n "$entries" ]; then
|
|
@@ -658,14 +699,18 @@ $target_dir/" "$gitignore"
|
|
|
658
699
|
if [ -d "$project_root/docs" ] && ! grep -qx "docs/" "$gitignore" 2>/dev/null; then
|
|
659
700
|
echo ""
|
|
660
701
|
echo " docs/ contains agent outputs (brainstorms, plans, research)."
|
|
661
|
-
|
|
702
|
+
if [ -t 0 ]; then
|
|
703
|
+
read -r -p " Track docs/ in git? [Y/n]: " docs_choice
|
|
704
|
+
else
|
|
705
|
+
docs_choice="Y"
|
|
706
|
+
fi
|
|
662
707
|
case "$docs_choice" in
|
|
663
708
|
[nN]*)
|
|
664
709
|
echo "docs/" >> "$gitignore"
|
|
665
710
|
green " Added docs/ to .gitignore"
|
|
666
711
|
;;
|
|
667
712
|
*)
|
|
668
|
-
green " docs/ will be tracked in git"
|
|
713
|
+
green " docs/ will be tracked in git (default)"
|
|
669
714
|
;;
|
|
670
715
|
esac
|
|
671
716
|
fi
|
|
@@ -876,6 +921,10 @@ cleanup_update() {
|
|
|
876
921
|
}
|
|
877
922
|
|
|
878
923
|
update_source_git() {
|
|
924
|
+
if ! git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
925
|
+
echo " CortexHawk installed via npm — run: npm update -g cortexhawk"
|
|
926
|
+
exit 0
|
|
927
|
+
fi
|
|
879
928
|
local current_branch
|
|
880
929
|
current_branch=$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
|
881
930
|
if [ "$current_branch" != "main" ]; then
|
|
@@ -888,7 +937,8 @@ update_source_git() {
|
|
|
888
937
|
}
|
|
889
938
|
|
|
890
939
|
update_source_release() {
|
|
891
|
-
local tmp_dir
|
|
940
|
+
local tmp_dir
|
|
941
|
+
tmp_dir=$(mktemp -d /tmp/cortexhawk-update-XXXXXX)
|
|
892
942
|
local repo_url
|
|
893
943
|
repo_url=$(get_source_url)
|
|
894
944
|
if [ -z "$repo_url" ] && [ -f "$TARGET/.cortexhawk-manifest" ]; then
|
|
@@ -899,13 +949,19 @@ update_source_release() {
|
|
|
899
949
|
echo "Set CORTEXHAWK_REPO environment variable and retry"
|
|
900
950
|
exit 1
|
|
901
951
|
fi
|
|
902
|
-
mkdir -p "$tmp_dir"
|
|
903
952
|
echo " Downloading from $repo_url..."
|
|
904
|
-
|
|
953
|
+
local tarball="$tmp_dir/update.tar.gz"
|
|
954
|
+
if ! curl -sL "$repo_url/archive/refs/heads/main.tar.gz" -o "$tarball"; then
|
|
905
955
|
echo "Error: failed to download latest release"
|
|
906
956
|
rm -rf "$tmp_dir"
|
|
907
957
|
exit 1
|
|
908
958
|
fi
|
|
959
|
+
if ! tar xzf "$tarball" -C "$tmp_dir" --strip-components=1; then
|
|
960
|
+
echo "Error: failed to extract update archive"
|
|
961
|
+
rm -rf "$tmp_dir"
|
|
962
|
+
exit 1
|
|
963
|
+
fi
|
|
964
|
+
rm -f "$tarball"
|
|
909
965
|
SCRIPT_DIR="$tmp_dir"
|
|
910
966
|
UPDATE_CLEANUP_DIR="$tmp_dir"
|
|
911
967
|
}
|
|
@@ -1097,431 +1153,46 @@ sync_skills_update() {
|
|
|
1097
1153
|
fi
|
|
1098
1154
|
}
|
|
1099
1155
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
fi
|
|
1113
|
-
|
|
1114
|
-
if [ ! -d "$TARGET" ]; then
|
|
1115
|
-
echo "Error: no CortexHawk installation found at $TARGET"
|
|
1116
|
-
echo "Run install.sh without --update for a fresh install"
|
|
1117
|
-
exit 1
|
|
1118
|
-
fi
|
|
1119
|
-
|
|
1120
|
-
# 2. Read manifest
|
|
1121
|
-
local manifest="$TARGET/.cortexhawk-manifest"
|
|
1122
|
-
local current_version="unknown"
|
|
1123
|
-
local current_profile="all"
|
|
1124
|
-
local source_type
|
|
1125
|
-
source_type=$(detect_source_type)
|
|
1126
|
-
|
|
1127
|
-
if [ -f "$manifest" ]; then
|
|
1128
|
-
current_version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1129
|
-
current_profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1130
|
-
local manifest_source
|
|
1131
|
-
manifest_source=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1132
|
-
[ -n "$manifest_source" ] && source_type="$manifest_source"
|
|
1133
|
-
else
|
|
1134
|
-
echo " No manifest found — treating as pre-update installation"
|
|
1135
|
-
fi
|
|
1136
|
-
|
|
1137
|
-
# 3. Profile override
|
|
1138
|
-
local update_profile="$current_profile"
|
|
1139
|
-
if [ -n "$PROFILE" ]; then
|
|
1140
|
-
update_profile="$PROFILE"
|
|
1141
|
-
fi
|
|
1142
|
-
|
|
1143
|
-
if [ "$DRY_RUN" = true ]; then
|
|
1144
|
-
echo "CortexHawk Dry Run (update)"
|
|
1145
|
-
echo "============================"
|
|
1146
|
-
else
|
|
1147
|
-
echo "CortexHawk Update"
|
|
1148
|
-
echo "==================="
|
|
1149
|
-
fi
|
|
1150
|
-
echo " Current version: $current_version"
|
|
1151
|
-
echo " Profile: $update_profile"
|
|
1152
|
-
echo " Source: $source_type"
|
|
1153
|
-
echo ""
|
|
1154
|
-
|
|
1155
|
-
# 3b. Auto-snapshot before update (skip in dry-run)
|
|
1156
|
-
if [ "$DRY_RUN" != true ] && [ -f "$manifest" ]; then
|
|
1157
|
-
echo "Creating pre-update snapshot..."
|
|
1158
|
-
do_snapshot 2>/dev/null
|
|
1159
|
-
PRE_UPDATE_SNAP=$(ls -t "$TARGET/.cortexhawk-snapshots"/*.json 2>/dev/null | head -1)
|
|
1160
|
-
[ -n "$PRE_UPDATE_SNAP" ] && echo " Saved: $(basename "$PRE_UPDATE_SNAP")"
|
|
1161
|
-
echo ""
|
|
1162
|
-
fi
|
|
1163
|
-
|
|
1164
|
-
# 4. Pull source (skip in dry-run — compare against current source)
|
|
1165
|
-
if [ "$DRY_RUN" != true ]; then
|
|
1166
|
-
if [ "$source_type" = "git" ]; then
|
|
1167
|
-
echo "Updating CortexHawk source via git pull..."
|
|
1168
|
-
update_source_git
|
|
1156
|
+
# === Generic Component Operations ===
|
|
1157
|
+
# These use the COMPONENTS array to avoid hardcoding directory lists.
|
|
1158
|
+
|
|
1159
|
+
# copy_all_components(source_dir, target_dir, profile)
|
|
1160
|
+
copy_all_components() {
|
|
1161
|
+
local src="$1" dst="$2" profile="$3"
|
|
1162
|
+
for entry in "${COMPONENTS[@]}"; do
|
|
1163
|
+
IFS=':' read -r name exec_flag <<< "$entry"
|
|
1164
|
+
[ -d "$src/$name" ] || continue
|
|
1165
|
+
mkdir -p "$dst/$name"
|
|
1166
|
+
if [ "$name" = "skills" ] && [ -n "$profile" ]; then
|
|
1167
|
+
copy_skills "$dst" "$profile" "$src"
|
|
1169
1168
|
else
|
|
1170
|
-
|
|
1171
|
-
update_source_release
|
|
1172
|
-
fi
|
|
1173
|
-
echo " Source updated successfully."
|
|
1174
|
-
fi
|
|
1175
|
-
|
|
1176
|
-
# 5. Compare versions
|
|
1177
|
-
local new_version
|
|
1178
|
-
new_version=$(get_version)
|
|
1179
|
-
echo " New version: $new_version"
|
|
1180
|
-
echo ""
|
|
1181
|
-
|
|
1182
|
-
if [ "$current_version" = "$new_version" ] && [ "$FORCE_MODE" != true ]; then
|
|
1183
|
-
# Same version — check if files actually changed (checksum comparison)
|
|
1184
|
-
local files_changed=0
|
|
1185
|
-
if [ -f "$manifest" ]; then
|
|
1186
|
-
while IFS= read -r line; do
|
|
1187
|
-
local fpath fhash
|
|
1188
|
-
fpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
|
|
1189
|
-
fhash=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
|
|
1190
|
-
[ -z "$fpath" ] || [ -z "$fhash" ] && continue
|
|
1191
|
-
local source_file="$SCRIPT_DIR/$fpath"
|
|
1192
|
-
[ -f "$source_file" ] || continue
|
|
1193
|
-
local source_hash
|
|
1194
|
-
source_hash=$(compute_checksum "$source_file")
|
|
1195
|
-
if [ "$fhash" != "$source_hash" ]; then
|
|
1196
|
-
files_changed=$((files_changed + 1))
|
|
1197
|
-
fi
|
|
1198
|
-
done < <(grep '"sha256:' "$manifest")
|
|
1199
|
-
fi
|
|
1200
|
-
|
|
1201
|
-
if [ "$files_changed" -eq 0 ] && [ "$DRY_RUN" != true ]; then
|
|
1202
|
-
# Still apply install improvements even if no component files changed
|
|
1203
|
-
local target_dir_name=".${TARGET_CLI:-claude}"
|
|
1204
|
-
update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
|
|
1205
|
-
[ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
|
|
1206
|
-
if [ ! -f "$TARGET/git-workflow.conf" ]; then
|
|
1207
|
-
GIT_BRANCHING="direct-main"
|
|
1208
|
-
GIT_COMMIT_CONVENTION="conventional"
|
|
1209
|
-
GIT_PR_PREFERENCE="on-demand"
|
|
1210
|
-
GIT_AUTO_PUSH="after-commit"
|
|
1211
|
-
GIT_WORK_BRANCH=""
|
|
1212
|
-
source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$(dirname "$TARGET")" "$TARGET"
|
|
1213
|
-
fi
|
|
1214
|
-
echo "Already up to date ($new_version). No component files changed."
|
|
1215
|
-
cleanup_update
|
|
1216
|
-
exit 0
|
|
1217
|
-
elif [ "$files_changed" -gt 0 ]; then
|
|
1218
|
-
echo " Same version ($new_version) but $files_changed file(s) changed in source."
|
|
1219
|
-
fi
|
|
1220
|
-
fi
|
|
1221
|
-
|
|
1222
|
-
if [ "$DRY_RUN" = true ]; then
|
|
1223
|
-
echo " Comparing source $new_version vs installed $current_version"
|
|
1224
|
-
elif [ "$current_version" = "$new_version" ]; then
|
|
1225
|
-
echo " Syncing changed files ($new_version)..."
|
|
1226
|
-
else
|
|
1227
|
-
echo " Updating $current_version -> $new_version"
|
|
1228
|
-
fi
|
|
1229
|
-
echo ""
|
|
1230
|
-
|
|
1231
|
-
# Set up profile file for skill filtering
|
|
1232
|
-
if [ -n "$update_profile" ] && [ "$update_profile" != "all" ]; then
|
|
1233
|
-
PROFILE_FILE="$SCRIPT_DIR/profiles/${update_profile}.json"
|
|
1234
|
-
if [ ! -f "$PROFILE_FILE" ]; then
|
|
1235
|
-
echo " Warning: profile '$update_profile' not found in source — installing all skills"
|
|
1236
|
-
update_profile="all"
|
|
1237
|
-
PROFILE_FILE=""
|
|
1169
|
+
cp -r "$src/$name/"* "$dst/$name/" 2>/dev/null || true
|
|
1238
1170
|
fi
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
# 6. Reset counters and sync components
|
|
1242
|
-
SYNC_ADDED=0
|
|
1243
|
-
SYNC_UPDATED=0
|
|
1244
|
-
SYNC_UNCHANGED=0
|
|
1245
|
-
SYNC_SKIPPED=0
|
|
1246
|
-
SYNC_CONFLICTS=0
|
|
1247
|
-
|
|
1248
|
-
sync_component "agents"
|
|
1249
|
-
sync_component "commands"
|
|
1250
|
-
sync_skills_update "$update_profile"
|
|
1251
|
-
sync_component "hooks"
|
|
1252
|
-
sync_component "modes"
|
|
1253
|
-
sync_component "mcp"
|
|
1254
|
-
|
|
1255
|
-
# 6b. Sync agent personas from project root
|
|
1256
|
-
local project_root
|
|
1257
|
-
project_root="$(dirname "$TARGET")"
|
|
1258
|
-
if [ -d "$project_root/.cortexhawk-agents" ] && [ "$DRY_RUN" != true ]; then
|
|
1259
|
-
cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
|
|
1260
|
-
local pc
|
|
1261
|
-
pc=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
1262
|
-
[ "$pc" -gt 0 ] && echo " Synced $pc agent persona(s) from .cortexhawk-agents/"
|
|
1263
|
-
fi
|
|
1264
|
-
|
|
1265
|
-
# 7. Detect removed upstream files
|
|
1266
|
-
if [ -f "$manifest" ]; then
|
|
1267
|
-
while IFS= read -r line; do
|
|
1268
|
-
local file_relpath
|
|
1269
|
-
file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/')
|
|
1270
|
-
[ -z "$file_relpath" ] && continue
|
|
1271
|
-
if [ ! -f "$SCRIPT_DIR/$file_relpath" ] && [ -f "$TARGET/$file_relpath" ]; then
|
|
1272
|
-
echo " Warning: $file_relpath was removed upstream (kept locally)"
|
|
1273
|
-
fi
|
|
1274
|
-
done < <(grep '"sha256:' "$manifest")
|
|
1275
|
-
fi
|
|
1276
|
-
|
|
1277
|
-
if [ "$DRY_RUN" != true ]; then
|
|
1278
|
-
# Make hooks executable
|
|
1279
|
-
chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
|
|
1280
|
-
|
|
1281
|
-
# 7b. Regenerate settings.json hooks + merge new permissions from compose.yml
|
|
1282
|
-
if [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
|
|
1283
|
-
local hooks_json
|
|
1284
|
-
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
1285
|
-
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
1286
|
-
echo "$hooks_json" | python3 -c "
|
|
1287
|
-
import json, sys
|
|
1288
|
-
hooks = json.load(sys.stdin)
|
|
1289
|
-
current = {}
|
|
1290
|
-
try:
|
|
1291
|
-
with open(sys.argv[1]) as f:
|
|
1292
|
-
current = json.load(f)
|
|
1293
|
-
except:
|
|
1294
|
-
pass
|
|
1295
|
-
current['hooks'] = hooks
|
|
1296
|
-
# Merge new permissions from source
|
|
1297
|
-
try:
|
|
1298
|
-
with open(sys.argv[2]) as f:
|
|
1299
|
-
src_perms = json.load(f).get('permissions', {})
|
|
1300
|
-
cur_perms = current.get('permissions', {})
|
|
1301
|
-
for key in ('allow', 'deny'):
|
|
1302
|
-
src_list = src_perms.get(key, [])
|
|
1303
|
-
cur_list = cur_perms.get(key, [])
|
|
1304
|
-
added = [p for p in src_list if p not in cur_list]
|
|
1305
|
-
if added:
|
|
1306
|
-
cur_list.extend(added)
|
|
1307
|
-
cur_perms[key] = cur_list
|
|
1308
|
-
current['permissions'] = cur_perms
|
|
1309
|
-
except:
|
|
1310
|
-
pass
|
|
1311
|
-
with open(sys.argv[1], 'w') as f:
|
|
1312
|
-
json.dump(current, f, indent=2)
|
|
1313
|
-
f.write('\n')
|
|
1314
|
-
" "$TARGET/settings.json" "$SCRIPT_DIR/settings.json"
|
|
1315
|
-
echo " Regenerated settings.json hooks from compose.yml"
|
|
1316
|
-
fi
|
|
1317
|
-
fi
|
|
1318
|
-
|
|
1319
|
-
# 8. Write new manifest
|
|
1320
|
-
write_manifest "$TARGET" "$update_profile" "$TARGET_CLI" true
|
|
1321
|
-
|
|
1322
|
-
# 9. Run audit
|
|
1323
|
-
run_audit "$(dirname "$TARGET")"
|
|
1324
|
-
|
|
1325
|
-
# 10. Apply install improvements (gitignore, git-workflow defaults)
|
|
1326
|
-
local target_dir_name=".${TARGET_CLI:-claude}"
|
|
1327
|
-
update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
|
|
1328
|
-
[ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
|
|
1329
|
-
setup_templates "$(dirname "$TARGET")"
|
|
1330
|
-
|
|
1331
|
-
if [ ! -f "$TARGET/git-workflow.conf" ]; then
|
|
1332
|
-
GIT_BRANCHING="direct-main"
|
|
1333
|
-
GIT_COMMIT_CONVENTION="conventional"
|
|
1334
|
-
GIT_PR_PREFERENCE="on-demand"
|
|
1335
|
-
GIT_AUTO_PUSH="after-commit"
|
|
1336
|
-
GIT_WORK_BRANCH=""
|
|
1337
|
-
source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$(dirname "$TARGET")" "$TARGET"
|
|
1338
|
-
fi
|
|
1339
|
-
fi
|
|
1340
|
-
|
|
1341
|
-
# 10. Print summary
|
|
1342
|
-
echo ""
|
|
1343
|
-
if [ "$DRY_RUN" = true ]; then
|
|
1344
|
-
echo "Dry run summary:"
|
|
1345
|
-
echo " Would add: $SYNC_ADDED"
|
|
1346
|
-
echo " Would update: $SYNC_UPDATED"
|
|
1347
|
-
echo " Unchanged: $SYNC_UNCHANGED"
|
|
1348
|
-
echo " Would skip: $SYNC_SKIPPED"
|
|
1349
|
-
echo " Conflicts: $SYNC_CONFLICTS"
|
|
1350
|
-
echo ""
|
|
1351
|
-
echo "No files were modified (dry run)."
|
|
1352
|
-
else
|
|
1353
|
-
echo "Update complete: $current_version -> $new_version"
|
|
1354
|
-
echo " Added: $SYNC_ADDED"
|
|
1355
|
-
echo " Updated: $SYNC_UPDATED"
|
|
1356
|
-
echo " Unchanged: $SYNC_UNCHANGED"
|
|
1357
|
-
echo " Skipped: $SYNC_SKIPPED"
|
|
1358
|
-
echo " Conflicts: $SYNC_CONFLICTS"
|
|
1359
|
-
if [ -n "${PRE_UPDATE_SNAP:-}" ]; then
|
|
1360
|
-
echo " Rollback: install.sh --restore $PRE_UPDATE_SNAP"
|
|
1361
|
-
fi
|
|
1362
|
-
echo ""
|
|
1363
|
-
echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
|
|
1364
|
-
fi
|
|
1365
|
-
|
|
1366
|
-
cleanup_update
|
|
1171
|
+
[ "$exec_flag" = "yes" ] && chmod +x "$dst/$name/"*.sh 2>/dev/null || true
|
|
1172
|
+
done
|
|
1367
1173
|
}
|
|
1368
1174
|
|
|
1369
|
-
#
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
ls -1t "$snap_dir"/*.json | tail -n "$to_delete" | while read -r old_snap; do
|
|
1381
|
-
rm -f "$old_snap"
|
|
1382
|
-
done
|
|
1383
|
-
echo " Rotated: removed $to_delete old snapshot(s), keeping $MAX_SNAPSHOTS"
|
|
1384
|
-
fi
|
|
1175
|
+
# sync_all_components(profile)
|
|
1176
|
+
sync_all_components() {
|
|
1177
|
+
local profile="$1"
|
|
1178
|
+
for entry in "${COMPONENTS[@]}"; do
|
|
1179
|
+
IFS=':' read -r name exec_flag <<< "$entry"
|
|
1180
|
+
if [ "$name" = "skills" ]; then
|
|
1181
|
+
sync_skills_update "$profile"
|
|
1182
|
+
else
|
|
1183
|
+
sync_component "$name"
|
|
1184
|
+
fi
|
|
1185
|
+
done
|
|
1385
1186
|
}
|
|
1386
1187
|
|
|
1387
|
-
#
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
local manifest="$TARGET/.cortexhawk-manifest"
|
|
1396
|
-
if [ ! -f "$manifest" ]; then
|
|
1397
|
-
echo "Error: no CortexHawk manifest found at $manifest"
|
|
1398
|
-
echo "Run install.sh first to create an installation"
|
|
1399
|
-
exit 1
|
|
1400
|
-
fi
|
|
1401
|
-
|
|
1402
|
-
# Read manifest metadata
|
|
1403
|
-
local version
|
|
1404
|
-
version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1405
|
-
local profile
|
|
1406
|
-
profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1407
|
-
local source_type
|
|
1408
|
-
source_type=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1409
|
-
local source_url
|
|
1410
|
-
source_url=$(grep '"source_url"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1411
|
-
local source_path
|
|
1412
|
-
source_path=$(grep '"source_path"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1413
|
-
|
|
1414
|
-
# Read settings.json
|
|
1415
|
-
local settings_json="null"
|
|
1416
|
-
if [ -f "$TARGET/settings.json" ]; then
|
|
1417
|
-
settings_json=$(cat "$TARGET/settings.json")
|
|
1418
|
-
fi
|
|
1419
|
-
|
|
1420
|
-
# Read git-workflow.conf
|
|
1421
|
-
local git_branching="" git_commit="" git_pr="" git_push=""
|
|
1422
|
-
if [ -f "$TARGET/git-workflow.conf" ]; then
|
|
1423
|
-
git_branching=$(grep '^BRANCHING=' "$TARGET/git-workflow.conf" | cut -d= -f2)
|
|
1424
|
-
git_commit=$(grep '^COMMIT_CONVENTION=' "$TARGET/git-workflow.conf" | cut -d= -f2)
|
|
1425
|
-
git_pr=$(grep '^PR_PREFERENCE=' "$TARGET/git-workflow.conf" | cut -d= -f2)
|
|
1426
|
-
git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
|
|
1427
|
-
fi
|
|
1428
|
-
|
|
1429
|
-
# Read custom profile if applicable (use PROFILE_FILE from current run, never glob /tmp/)
|
|
1430
|
-
local profile_def="null"
|
|
1431
|
-
if [ -n "$PROFILE_FILE" ] && [ -f "$PROFILE_FILE" ]; then
|
|
1432
|
-
profile_def=$(cat "$PROFILE_FILE")
|
|
1433
|
-
fi
|
|
1434
|
-
|
|
1435
|
-
# Build files checksums from manifest
|
|
1436
|
-
local files_json
|
|
1437
|
-
files_json=$(sed -n '/"files"/,/^ }/p' "$manifest" | sed '1d;$d')
|
|
1438
|
-
|
|
1439
|
-
# Collect file contents (base64 encoded for binary safety)
|
|
1440
|
-
local git_workflow_content="" claude_md_content=""
|
|
1441
|
-
if [ -f "$TARGET/git-workflow.conf" ]; then
|
|
1442
|
-
git_workflow_content=$(base64 < "$TARGET/git-workflow.conf" | tr -d '\n')
|
|
1443
|
-
fi
|
|
1444
|
-
if [ -f "$TARGET/../CLAUDE.md" ]; then
|
|
1445
|
-
claude_md_content=$(base64 < "$TARGET/../CLAUDE.md" | tr -d '\n')
|
|
1446
|
-
fi
|
|
1447
|
-
|
|
1448
|
-
# Generate snapshot
|
|
1449
|
-
local now
|
|
1450
|
-
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1451
|
-
local snap_name
|
|
1452
|
-
snap_name=$(date -u +"%Y-%m-%d-%H%M%S")
|
|
1453
|
-
local snap_dir="$TARGET/.cortexhawk-snapshots"
|
|
1454
|
-
local snap_file="$snap_dir/${snap_name}.json"
|
|
1455
|
-
|
|
1456
|
-
mkdir -p "$snap_dir"
|
|
1457
|
-
|
|
1458
|
-
# Write snapshot JSON
|
|
1459
|
-
printf '{\n' > "$snap_file"
|
|
1460
|
-
printf ' "snapshot_version": "2",\n' >> "$snap_file"
|
|
1461
|
-
printf ' "snapshot_date": "%s",\n' "$now" >> "$snap_file"
|
|
1462
|
-
printf ' "cortexhawk_version": "%s",\n' "$version" >> "$snap_file"
|
|
1463
|
-
printf ' "target": "claude",\n' >> "$snap_file"
|
|
1464
|
-
printf ' "profile": "%s",\n' "$profile" >> "$snap_file"
|
|
1465
|
-
printf ' "profile_definition": %s,\n' "$profile_def" >> "$snap_file"
|
|
1466
|
-
printf ' "source": "%s",\n' "$source_type" >> "$snap_file"
|
|
1467
|
-
printf ' "source_url": "%s",\n' "$source_url" >> "$snap_file"
|
|
1468
|
-
printf ' "source_path": "%s",\n' "$source_path" >> "$snap_file"
|
|
1469
|
-
printf ' "settings": %s,\n' "$settings_json" >> "$snap_file"
|
|
1470
|
-
printf ' "git_workflow": {\n' >> "$snap_file"
|
|
1471
|
-
printf ' "BRANCHING": "%s",\n' "$git_branching" >> "$snap_file"
|
|
1472
|
-
printf ' "COMMIT_CONVENTION": "%s",\n' "$git_commit" >> "$snap_file"
|
|
1473
|
-
printf ' "PR_PREFERENCE": "%s",\n' "$git_pr" >> "$snap_file"
|
|
1474
|
-
printf ' "AUTO_PUSH": "%s"\n' "$git_push" >> "$snap_file"
|
|
1475
|
-
printf ' },\n' >> "$snap_file"
|
|
1476
|
-
printf ' "files": {\n' >> "$snap_file"
|
|
1477
|
-
printf '%s\n' "$files_json" >> "$snap_file"
|
|
1478
|
-
printf ' },\n' >> "$snap_file"
|
|
1479
|
-
printf ' "file_contents": {\n' >> "$snap_file"
|
|
1480
|
-
[ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
|
|
1481
|
-
[ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
|
|
1482
|
-
# Remove trailing comma from last entry
|
|
1483
|
-
sed_inplace '$ s/,$//' "$snap_file"
|
|
1484
|
-
printf ' }\n' >> "$snap_file"
|
|
1485
|
-
printf '}\n' >> "$snap_file"
|
|
1486
|
-
|
|
1487
|
-
echo "CortexHawk Snapshot"
|
|
1488
|
-
echo "====================="
|
|
1489
|
-
echo " Version: $version"
|
|
1490
|
-
echo " Profile: $profile"
|
|
1491
|
-
echo " Target: $TARGET"
|
|
1492
|
-
echo " Saved to: $snap_file"
|
|
1493
|
-
|
|
1494
|
-
# Create portable archive if requested
|
|
1495
|
-
if [ "$PORTABLE_MODE" = true ]; then
|
|
1496
|
-
local archive_name="${snap_name}.tar.gz"
|
|
1497
|
-
local archive_path="$snap_dir/$archive_name"
|
|
1498
|
-
local tmp_dir
|
|
1499
|
-
tmp_dir=$(mktemp -d)
|
|
1500
|
-
|
|
1501
|
-
# Copy snapshot and files to temp structure
|
|
1502
|
-
cp "$snap_file" "$tmp_dir/snapshot.json"
|
|
1503
|
-
mkdir -p "$tmp_dir/files"
|
|
1504
|
-
cp -r "$TARGET/agents" "$tmp_dir/files/" 2>/dev/null || true
|
|
1505
|
-
cp -r "$TARGET/commands" "$tmp_dir/files/" 2>/dev/null || true
|
|
1506
|
-
cp -r "$TARGET/skills" "$tmp_dir/files/" 2>/dev/null || true
|
|
1507
|
-
cp -r "$TARGET/hooks" "$tmp_dir/files/" 2>/dev/null || true
|
|
1508
|
-
cp -r "$TARGET/modes" "$tmp_dir/files/" 2>/dev/null || true
|
|
1509
|
-
cp -r "$TARGET/mcp" "$tmp_dir/files/" 2>/dev/null || true
|
|
1510
|
-
cp "$TARGET/settings.json" "$tmp_dir/files/" 2>/dev/null || true
|
|
1511
|
-
cp "$TARGET/git-workflow.conf" "$tmp_dir/files/" 2>/dev/null || true
|
|
1512
|
-
|
|
1513
|
-
# Create archive
|
|
1514
|
-
tar -czf "$archive_path" -C "$tmp_dir" .
|
|
1515
|
-
rm -rf "$tmp_dir"
|
|
1516
|
-
|
|
1517
|
-
echo " Archive: $archive_path"
|
|
1518
|
-
echo ""
|
|
1519
|
-
echo "Restore with: install.sh --restore $archive_path"
|
|
1520
|
-
else
|
|
1521
|
-
rotate_snapshots "$snap_dir"
|
|
1522
|
-
echo ""
|
|
1523
|
-
echo "Restore with: install.sh --restore $snap_file"
|
|
1524
|
-
fi
|
|
1188
|
+
# count_component_files(source_dir)
|
|
1189
|
+
count_component_files() {
|
|
1190
|
+
local src="$1"
|
|
1191
|
+
for entry in "${COMPONENTS[@]}"; do
|
|
1192
|
+
IFS=':' read -r name _ <<< "$entry"
|
|
1193
|
+
local c; c=$(find "$src/$name" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
1194
|
+
printf " %-12s %s files\n" "$name/" "$c"
|
|
1195
|
+
done
|
|
1525
1196
|
}
|
|
1526
1197
|
|
|
1527
1198
|
# --- do_snapshots_list() ---
|
|
@@ -1873,370 +1544,6 @@ do_export_team() {
|
|
|
1873
1544
|
echo "Share this file with your team. Install with: install.sh --team"
|
|
1874
1545
|
}
|
|
1875
1546
|
|
|
1876
|
-
# --- do_restore() ---
|
|
1877
|
-
do_restore() {
|
|
1878
|
-
local snap_file="$1"
|
|
1879
|
-
local archive_tmp=""
|
|
1880
|
-
local portable_files=""
|
|
1881
|
-
|
|
1882
|
-
if [ -z "$snap_file" ] || [ ! -f "$snap_file" ]; then
|
|
1883
|
-
echo "Error: snapshot file not found: $snap_file"
|
|
1884
|
-
exit 1
|
|
1885
|
-
fi
|
|
1886
|
-
|
|
1887
|
-
# Handle portable archive (.tar.gz)
|
|
1888
|
-
if [[ "$snap_file" == *.tar.gz ]]; then
|
|
1889
|
-
archive_tmp=$(mktemp -d)
|
|
1890
|
-
tar -xzf "$snap_file" -C "$archive_tmp"
|
|
1891
|
-
snap_file="$archive_tmp/snapshot.json"
|
|
1892
|
-
portable_files="$archive_tmp/files"
|
|
1893
|
-
if [ ! -f "$snap_file" ]; then
|
|
1894
|
-
echo "Error: invalid archive — snapshot.json not found"
|
|
1895
|
-
rm -rf "$archive_tmp"
|
|
1896
|
-
exit 1
|
|
1897
|
-
fi
|
|
1898
|
-
echo "Extracting portable archive..."
|
|
1899
|
-
fi
|
|
1900
|
-
|
|
1901
|
-
# Extract metadata from snapshot
|
|
1902
|
-
local snap_version
|
|
1903
|
-
snap_version=$(grep '"cortexhawk_version"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1904
|
-
local snap_profile
|
|
1905
|
-
snap_profile=$(grep '"profile"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1906
|
-
local snap_source
|
|
1907
|
-
snap_source=$(grep '"source"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1908
|
-
local snap_source_url
|
|
1909
|
-
snap_source_url=$(grep '"source_url"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1910
|
-
local snap_source_path
|
|
1911
|
-
snap_source_path=$(grep '"source_path"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
1912
|
-
|
|
1913
|
-
echo "CortexHawk Restore"
|
|
1914
|
-
echo "====================="
|
|
1915
|
-
echo " Snapshot version: $snap_version"
|
|
1916
|
-
echo " Profile: $snap_profile"
|
|
1917
|
-
echo " Source: $snap_source"
|
|
1918
|
-
echo ""
|
|
1919
|
-
|
|
1920
|
-
# Determine CortexHawk source
|
|
1921
|
-
local restore_source="$SCRIPT_DIR"
|
|
1922
|
-
if [ "$snap_source" = "git" ] && [ -n "$snap_source_path" ] && [ -d "$snap_source_path" ]; then
|
|
1923
|
-
restore_source="$snap_source_path"
|
|
1924
|
-
fi
|
|
1925
|
-
if [ ! -d "$restore_source/agents" ]; then
|
|
1926
|
-
echo "Error: CortexHawk source not found at $restore_source"
|
|
1927
|
-
echo "Ensure the CortexHawk repo is available or set CORTEXHAWK_REPO"
|
|
1928
|
-
exit 1
|
|
1929
|
-
fi
|
|
1930
|
-
|
|
1931
|
-
# Warn if source version differs from snapshot
|
|
1932
|
-
local current_version
|
|
1933
|
-
current_version=$(get_version)
|
|
1934
|
-
if [ "$snap_version" != "$current_version" ]; then
|
|
1935
|
-
echo " Warning: snapshot is v$snap_version but source is v$current_version"
|
|
1936
|
-
echo " Some file checksums may not match"
|
|
1937
|
-
echo ""
|
|
1938
|
-
fi
|
|
1939
|
-
|
|
1940
|
-
# Set profile for reinstall
|
|
1941
|
-
if [ -n "$snap_profile" ] && [ "$snap_profile" != "all" ]; then
|
|
1942
|
-
PROFILE="$snap_profile"
|
|
1943
|
-
PROFILE_FILE="$restore_source/profiles/${snap_profile}.json"
|
|
1944
|
-
if [ ! -f "$PROFILE_FILE" ]; then
|
|
1945
|
-
echo " Warning: profile '$snap_profile' not found — installing all skills"
|
|
1946
|
-
PROFILE=""
|
|
1947
|
-
PROFILE_FILE=""
|
|
1948
|
-
fi
|
|
1949
|
-
fi
|
|
1950
|
-
|
|
1951
|
-
# Determine target
|
|
1952
|
-
if [ "$GLOBAL" = true ]; then
|
|
1953
|
-
TARGET="$HOME/.claude"
|
|
1954
|
-
else
|
|
1955
|
-
TARGET="$(pwd)/.claude"
|
|
1956
|
-
fi
|
|
1957
|
-
|
|
1958
|
-
# Save the original SCRIPT_DIR, use snapshot source
|
|
1959
|
-
local orig_script_dir="$SCRIPT_DIR"
|
|
1960
|
-
SCRIPT_DIR="$restore_source"
|
|
1961
|
-
|
|
1962
|
-
# Reinstall using the standard flow
|
|
1963
|
-
echo "Reinstalling CortexHawk components..."
|
|
1964
|
-
mkdir -p "$TARGET"/{agents,commands,skills,hooks,modes,mcp}
|
|
1965
|
-
|
|
1966
|
-
# Use portable archive files if available, otherwise use source repo
|
|
1967
|
-
if [ -n "$portable_files" ] && [ -d "$portable_files" ]; then
|
|
1968
|
-
echo " Using files from portable archive..."
|
|
1969
|
-
cp -r "$portable_files/agents/"* "$TARGET/agents/" 2>/dev/null || true
|
|
1970
|
-
cp -r "$portable_files/commands/"* "$TARGET/commands/" 2>/dev/null || true
|
|
1971
|
-
cp -r "$portable_files/skills/"* "$TARGET/skills/" 2>/dev/null || true
|
|
1972
|
-
cp -r "$portable_files/hooks/"* "$TARGET/hooks/" 2>/dev/null || true
|
|
1973
|
-
cp -r "$portable_files/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
1974
|
-
cp -r "$portable_files/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
1975
|
-
else
|
|
1976
|
-
cp -r "$SCRIPT_DIR/agents/"* "$TARGET/agents/" 2>/dev/null || true
|
|
1977
|
-
cp -r "$SCRIPT_DIR/commands/"* "$TARGET/commands/" 2>/dev/null || true
|
|
1978
|
-
copy_skills "$TARGET" "$PROFILE"
|
|
1979
|
-
cp -r "$SCRIPT_DIR/hooks/"* "$TARGET/hooks/" 2>/dev/null || true
|
|
1980
|
-
cp -r "$SCRIPT_DIR/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
1981
|
-
cp -r "$SCRIPT_DIR/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
1982
|
-
fi
|
|
1983
|
-
chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
|
|
1984
|
-
|
|
1985
|
-
# Restore settings.json from snapshot
|
|
1986
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
1987
|
-
python3 -c "
|
|
1988
|
-
import json, sys
|
|
1989
|
-
with open(sys.argv[1]) as f:
|
|
1990
|
-
snap = json.load(f)
|
|
1991
|
-
settings = snap.get('settings')
|
|
1992
|
-
if settings is not None:
|
|
1993
|
-
with open(sys.argv[2], 'w') as f:
|
|
1994
|
-
json.dump(settings, f, indent=2)
|
|
1995
|
-
f.write('\n')
|
|
1996
|
-
print(' Restored settings.json')
|
|
1997
|
-
" "$snap_file" "$TARGET/settings.json"
|
|
1998
|
-
else
|
|
1999
|
-
echo " Warning: python3 not found — settings.json not restored from snapshot"
|
|
2000
|
-
fi
|
|
2001
|
-
|
|
2002
|
-
# Restore git-workflow.conf from snapshot
|
|
2003
|
-
local branching commit_conv pr_pref auto_push
|
|
2004
|
-
branching=$(grep '"BRANCHING"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
2005
|
-
commit_conv=$(grep '"COMMIT_CONVENTION"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
2006
|
-
pr_pref=$(grep '"PR_PREFERENCE"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
2007
|
-
auto_push=$(grep '"AUTO_PUSH"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
|
|
2008
|
-
|
|
2009
|
-
if [ -n "$branching" ] || [ -n "$commit_conv" ] || [ -n "$pr_pref" ] || [ -n "$auto_push" ]; then
|
|
2010
|
-
{
|
|
2011
|
-
echo "BRANCHING=$branching"
|
|
2012
|
-
echo "COMMIT_CONVENTION=$commit_conv"
|
|
2013
|
-
echo "PR_PREFERENCE=$pr_pref"
|
|
2014
|
-
echo "AUTO_PUSH=$auto_push"
|
|
2015
|
-
} > "$TARGET/git-workflow.conf"
|
|
2016
|
-
echo " Restored git-workflow.conf (from git_workflow keys)"
|
|
2017
|
-
fi
|
|
2018
|
-
|
|
2019
|
-
# Restore file_contents (snapshot v2) — overwrites git-workflow.conf if present
|
|
2020
|
-
if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
|
|
2021
|
-
python3 -c "
|
|
2022
|
-
import json, base64, sys, os
|
|
2023
|
-
snap_file, target_dir = sys.argv[1], sys.argv[2]
|
|
2024
|
-
with open(snap_file) as f:
|
|
2025
|
-
snap = json.load(f)
|
|
2026
|
-
contents = snap.get('file_contents', {})
|
|
2027
|
-
for filename, b64data in contents.items():
|
|
2028
|
-
try:
|
|
2029
|
-
data = base64.b64decode(b64data).decode('utf-8')
|
|
2030
|
-
if filename == 'CLAUDE.md':
|
|
2031
|
-
target_path = os.path.dirname(target_dir) + '/CLAUDE.md'
|
|
2032
|
-
else:
|
|
2033
|
-
target_path = target_dir + '/' + filename
|
|
2034
|
-
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
2035
|
-
with open(target_path, 'w') as f:
|
|
2036
|
-
f.write(data)
|
|
2037
|
-
print(f' Restored {filename} (from file_contents)')
|
|
2038
|
-
except Exception as e:
|
|
2039
|
-
print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
|
|
2040
|
-
" "$snap_file" "$TARGET"
|
|
2041
|
-
fi
|
|
2042
|
-
|
|
2043
|
-
# Write new manifest
|
|
2044
|
-
write_manifest "$TARGET" "$PROFILE" "claude" false
|
|
2045
|
-
|
|
2046
|
-
# Verify checksums against snapshot
|
|
2047
|
-
local verified=0 mismatched=0 missing=0
|
|
2048
|
-
while IFS= read -r line; do
|
|
2049
|
-
local file_relpath
|
|
2050
|
-
file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
|
|
2051
|
-
local expected_checksum
|
|
2052
|
-
expected_checksum=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
|
|
2053
|
-
[ -z "$file_relpath" ] || [ -z "$expected_checksum" ] && continue
|
|
2054
|
-
|
|
2055
|
-
local target_file="$TARGET/$file_relpath"
|
|
2056
|
-
if [ ! -f "$target_file" ]; then
|
|
2057
|
-
missing=$((missing + 1))
|
|
2058
|
-
else
|
|
2059
|
-
local actual_checksum
|
|
2060
|
-
actual_checksum=$(compute_checksum "$target_file")
|
|
2061
|
-
if [ "$actual_checksum" = "$expected_checksum" ]; then
|
|
2062
|
-
verified=$((verified + 1))
|
|
2063
|
-
else
|
|
2064
|
-
mismatched=$((mismatched + 1))
|
|
2065
|
-
fi
|
|
2066
|
-
fi
|
|
2067
|
-
done < <(grep '"sha256:' "$snap_file")
|
|
2068
|
-
|
|
2069
|
-
SCRIPT_DIR="$orig_script_dir"
|
|
2070
|
-
|
|
2071
|
-
echo ""
|
|
2072
|
-
echo "Restore complete"
|
|
2073
|
-
echo " Verified: $verified files match snapshot checksums"
|
|
2074
|
-
[ "$mismatched" -gt 0 ] && echo " Mismatched: $mismatched files differ (source version may differ from snapshot)"
|
|
2075
|
-
[ "$missing" -gt 0 ] && echo " Missing: $missing files not found in source"
|
|
2076
|
-
echo ""
|
|
2077
|
-
echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
|
|
2078
|
-
|
|
2079
|
-
# Cleanup temp dir from portable archive
|
|
2080
|
-
[ -n "$archive_tmp" ] && rm -rf "$archive_tmp"
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
# --- do_doctor() ---
|
|
2084
|
-
# Diagnose installation health
|
|
2085
|
-
do_doctor() {
|
|
2086
|
-
if [ "$GLOBAL" = true ]; then
|
|
2087
|
-
TARGET="$HOME/.claude"
|
|
2088
|
-
else
|
|
2089
|
-
TARGET="$(pwd)/.claude"
|
|
2090
|
-
fi
|
|
2091
|
-
|
|
2092
|
-
local ok=0 warn=0 err=0
|
|
2093
|
-
_doc_ok() { echo " [OK] $1"; ok=$((ok+1)); }
|
|
2094
|
-
_doc_warn() { echo " [WARN] $1"; warn=$((warn+1)); }
|
|
2095
|
-
_doc_err() { echo " [ERR] $1"; err=$((err+1)); }
|
|
2096
|
-
|
|
2097
|
-
# Header
|
|
2098
|
-
local version="" profile="" target_cli_name=""
|
|
2099
|
-
if [ -f "$TARGET/.cortexhawk-manifest" ]; then
|
|
2100
|
-
version=$(grep -o '"version": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
|
|
2101
|
-
profile=$(grep -o '"profile": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
|
|
2102
|
-
target_cli_name=$(grep -o '"target": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
|
|
2103
|
-
fi
|
|
2104
|
-
echo "CortexHawk Doctor"
|
|
2105
|
-
echo "==================="
|
|
2106
|
-
echo " Installation: $TARGET"
|
|
2107
|
-
echo " Version: ${version:-unknown}"
|
|
2108
|
-
echo " Profile: ${profile:-unknown}"
|
|
2109
|
-
echo " Target: ${target_cli_name:-claude}"
|
|
2110
|
-
echo ""
|
|
2111
|
-
echo "Checks:"
|
|
2112
|
-
|
|
2113
|
-
# 1. Manifest
|
|
2114
|
-
if [ -f "$TARGET/.cortexhawk-manifest" ]; then
|
|
2115
|
-
_doc_ok "Manifest present"
|
|
2116
|
-
else
|
|
2117
|
-
_doc_err "Manifest missing — run install.sh to create an installation"
|
|
2118
|
-
fi
|
|
2119
|
-
|
|
2120
|
-
# 2. settings.json valid JSON
|
|
2121
|
-
if [ -f "$TARGET/settings.json" ]; then
|
|
2122
|
-
if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$TARGET/settings.json" 2>/dev/null; then
|
|
2123
|
-
_doc_ok "settings.json valid JSON"
|
|
2124
|
-
else
|
|
2125
|
-
_doc_err "settings.json invalid JSON"
|
|
2126
|
-
fi
|
|
2127
|
-
else
|
|
2128
|
-
_doc_warn "settings.json not found"
|
|
2129
|
-
fi
|
|
2130
|
-
|
|
2131
|
-
# 3. Component counts (compare installed vs source)
|
|
2132
|
-
for comp in agents commands modes; do
|
|
2133
|
-
local installed=0 source_count=0
|
|
2134
|
-
installed=$(find "$TARGET/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
2135
|
-
source_count=$(find "$SCRIPT_DIR/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
2136
|
-
if [ "$installed" -eq "$source_count" ] 2>/dev/null; then
|
|
2137
|
-
_doc_ok "$installed/$source_count $comp installed"
|
|
2138
|
-
elif [ "$installed" -gt 0 ] 2>/dev/null; then
|
|
2139
|
-
_doc_warn "$installed/$source_count $comp installed"
|
|
2140
|
-
else
|
|
2141
|
-
_doc_err "0/$source_count $comp installed"
|
|
2142
|
-
fi
|
|
2143
|
-
done
|
|
2144
|
-
|
|
2145
|
-
# 4. Skills (profile-dependent, just count what's there)
|
|
2146
|
-
local skills_installed=0 skills_source=0
|
|
2147
|
-
skills_installed=$(find "$TARGET/skills" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
2148
|
-
skills_source=$(find "$SCRIPT_DIR/skills" -name "*.md" -type f 2>/dev/null | wc -l)
|
|
2149
|
-
if [ "$skills_installed" -gt 0 ] 2>/dev/null; then
|
|
2150
|
-
_doc_ok "$skills_installed/$skills_source skills installed (profile: ${profile:-all})"
|
|
2151
|
-
else
|
|
2152
|
-
_doc_err "No skills installed"
|
|
2153
|
-
fi
|
|
2154
|
-
|
|
2155
|
-
# 5. Hooks executable
|
|
2156
|
-
local hooks_ok=0 hooks_total=0
|
|
2157
|
-
for hook in "$TARGET/hooks/"*.sh; do
|
|
2158
|
-
[ -f "$hook" ] || continue
|
|
2159
|
-
hooks_total=$((hooks_total+1))
|
|
2160
|
-
if [ -x "$hook" ]; then
|
|
2161
|
-
hooks_ok=$((hooks_ok+1))
|
|
2162
|
-
else
|
|
2163
|
-
_doc_warn "Hook not executable: $(basename "$hook")"
|
|
2164
|
-
fi
|
|
2165
|
-
done
|
|
2166
|
-
if [ "$hooks_total" -gt 0 ]; then
|
|
2167
|
-
if [ "$hooks_ok" -eq "$hooks_total" ]; then
|
|
2168
|
-
_doc_ok "$hooks_ok/$hooks_total hooks executable"
|
|
2169
|
-
fi
|
|
2170
|
-
else
|
|
2171
|
-
_doc_warn "No hooks found"
|
|
2172
|
-
fi
|
|
2173
|
-
|
|
2174
|
-
# 6. compose.yml vs settings.json coherence
|
|
2175
|
-
if [ -f "$TARGET/../hooks/compose.yml" ] || [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
|
|
2176
|
-
_doc_ok "compose.yml present"
|
|
2177
|
-
fi
|
|
2178
|
-
|
|
2179
|
-
# 7. MCP configs
|
|
2180
|
-
if [ -d "$TARGET/mcp" ] && [ "$(find "$TARGET/mcp" -type f 2>/dev/null | wc -l)" -gt 0 ]; then
|
|
2181
|
-
_doc_ok "MCP configs present"
|
|
2182
|
-
elif [ -d "$TARGET/mcp" ]; then
|
|
2183
|
-
_doc_warn "MCP directory exists but empty"
|
|
2184
|
-
fi
|
|
2185
|
-
|
|
2186
|
-
# 8. docs/ workspace
|
|
2187
|
-
local project_root
|
|
2188
|
-
project_root="$(dirname "$TARGET")"
|
|
2189
|
-
if [ -d "$project_root/docs" ]; then
|
|
2190
|
-
_doc_ok "docs/ workspace exists"
|
|
2191
|
-
else
|
|
2192
|
-
_doc_warn "docs/ workspace missing"
|
|
2193
|
-
fi
|
|
2194
|
-
|
|
2195
|
-
# 9. Broken symlinks in docs/plans/
|
|
2196
|
-
local broken=0
|
|
2197
|
-
if [ -d "$project_root/docs/plans" ]; then
|
|
2198
|
-
while IFS= read -r link; do
|
|
2199
|
-
[ -z "$link" ] && continue
|
|
2200
|
-
_doc_warn "Broken symlink: $link"
|
|
2201
|
-
broken=$((broken+1))
|
|
2202
|
-
done < <(find "$project_root/docs/plans" -type l ! -exec test -e {} \; -print 2>/dev/null)
|
|
2203
|
-
[ "$broken" -eq 0 ] && _doc_ok "No broken symlinks in docs/plans/"
|
|
2204
|
-
fi
|
|
2205
|
-
|
|
2206
|
-
# 10. git-workflow.conf
|
|
2207
|
-
if [ -f "$TARGET/git-workflow.conf" ]; then
|
|
2208
|
-
_doc_ok "git-workflow.conf present"
|
|
2209
|
-
else
|
|
2210
|
-
_doc_warn "git-workflow.conf not found (run --init to configure)"
|
|
2211
|
-
fi
|
|
2212
|
-
|
|
2213
|
-
# 11. CLAUDE.md at project root
|
|
2214
|
-
if [ -f "$project_root/CLAUDE.md" ]; then
|
|
2215
|
-
_doc_ok "CLAUDE.md present at project root"
|
|
2216
|
-
else
|
|
2217
|
-
_doc_warn "CLAUDE.md not found at project root"
|
|
2218
|
-
fi
|
|
2219
|
-
|
|
2220
|
-
# 12. Version match source vs manifest
|
|
2221
|
-
if [ -n "$version" ]; then
|
|
2222
|
-
local source_version
|
|
2223
|
-
source_version=$(get_version)
|
|
2224
|
-
if [ "$version" = "$source_version" ]; then
|
|
2225
|
-
_doc_ok "Version match: source $source_version = manifest $version"
|
|
2226
|
-
else
|
|
2227
|
-
_doc_warn "Version mismatch: source $source_version != manifest $version (run --update)"
|
|
2228
|
-
fi
|
|
2229
|
-
fi
|
|
2230
|
-
|
|
2231
|
-
# Summary
|
|
2232
|
-
echo ""
|
|
2233
|
-
echo "Summary: $ok OK, $warn WARN, $err ERR"
|
|
2234
|
-
|
|
2235
|
-
# Exit code: 1 if any errors
|
|
2236
|
-
[ "$err" -gt 0 ] && exit 1
|
|
2237
|
-
return 0
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
1547
|
# --- do_uninstall() ---
|
|
2241
1548
|
# Remove CortexHawk installation cleanly
|
|
2242
1549
|
do_uninstall() {
|
|
@@ -2707,10 +2014,17 @@ do_add_skill() {
|
|
|
2707
2014
|
exit 1
|
|
2708
2015
|
fi
|
|
2709
2016
|
|
|
2710
|
-
# Security
|
|
2017
|
+
# Security gate: block skills with executable scripts unless --trust is used
|
|
2711
2018
|
if find "$tmp_dir/repo" -name "*.sh" -o -name "*.py" 2>/dev/null | grep -q .; then
|
|
2712
|
-
|
|
2713
|
-
|
|
2019
|
+
if [ "${TRUST_SKILL:-false}" = true ]; then
|
|
2020
|
+
yellow " Warning: this skill contains executable scripts — installing with --trust"
|
|
2021
|
+
else
|
|
2022
|
+
echo " ERROR: This skill contains executable scripts (.sh/.py)."
|
|
2023
|
+
echo " Re-run with --trust to install: install.sh --add-skill $ADD_SKILL_URL --trust"
|
|
2024
|
+
echo " Review the source first: $ADD_SKILL_URL"
|
|
2025
|
+
rm -rf "$tmp_dir"
|
|
2026
|
+
exit 1
|
|
2027
|
+
fi
|
|
2714
2028
|
fi
|
|
2715
2029
|
|
|
2716
2030
|
# Create community directory if needed
|
|
@@ -2831,7 +2145,8 @@ do_team_install() {
|
|
|
2831
2145
|
|
|
2832
2146
|
# Generate temporary profile JSON from skills list
|
|
2833
2147
|
if [ -n "$TEAM_SKILLS" ]; then
|
|
2834
|
-
local tmp_profile
|
|
2148
|
+
local tmp_profile
|
|
2149
|
+
tmp_profile=$(mktemp /tmp/cortexhawk-team-XXXXXX.json)
|
|
2835
2150
|
printf '{\n "name": "team",\n "skills": [\n' > "$tmp_profile"
|
|
2836
2151
|
local first=true
|
|
2837
2152
|
while IFS= read -r skill; do
|
|
@@ -2907,172 +2222,37 @@ do_team_install() {
|
|
|
2907
2222
|
echo "Team install complete."
|
|
2908
2223
|
}
|
|
2909
2224
|
|
|
2910
|
-
# ---
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
if [ "$DRY_RUN" = true ]; then
|
|
2919
|
-
echo "CortexHawk Dry Run (install)"
|
|
2920
|
-
echo "=============================="
|
|
2921
|
-
echo " Target: $TARGET"
|
|
2922
|
-
echo " Profile: ${PROFILE:-all}"
|
|
2923
|
-
echo ""
|
|
2924
|
-
echo "Would install:"
|
|
2925
|
-
for comp in agents commands hooks modes mcp; do
|
|
2926
|
-
local c; c=$(find "$SCRIPT_DIR/$comp" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2927
|
-
printf " %-12s %s files\n" "$comp/" "$c"
|
|
2928
|
-
done
|
|
2929
|
-
local sc; sc=$(find "$SCRIPT_DIR/skills" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2930
|
-
printf " %-12s %s files\n" "skills/" "$sc"
|
|
2931
|
-
echo " settings.json"
|
|
2932
|
-
[ ! -f "$(pwd)/CLAUDE.md" ] && echo " CLAUDE.md"
|
|
2933
|
-
echo ""
|
|
2934
|
-
echo "No files were modified (dry run)."
|
|
2935
|
-
return
|
|
2225
|
+
# --- install_git_post_merge_hook(project_root) ---
|
|
2226
|
+
# Installs .git/hooks/post-merge to auto-run cleanup after git merge (opt-in)
|
|
2227
|
+
install_git_post_merge_hook() {
|
|
2228
|
+
local project_root="${1:-$(pwd)}"
|
|
2229
|
+
local git_dir="$project_root/.git"
|
|
2230
|
+
if [ ! -d "$git_dir" ]; then
|
|
2231
|
+
yellow " Warning: no .git directory found — skipping post-merge hook"
|
|
2232
|
+
return 0
|
|
2936
2233
|
fi
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
cp -r "$SCRIPT_DIR/agents/"* "$TARGET/agents/" 2>/dev/null || true
|
|
2943
|
-
# Copy agent personas from project root if present
|
|
2944
|
-
local project_root
|
|
2945
|
-
project_root="$(dirname "$TARGET")"
|
|
2946
|
-
if [ -d "$project_root/.cortexhawk-agents" ]; then
|
|
2947
|
-
cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
|
|
2948
|
-
local persona_count
|
|
2949
|
-
persona_count=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2950
|
-
[ "$persona_count" -gt 0 ] && echo " Loaded $persona_count agent persona(s) from .cortexhawk-agents/"
|
|
2951
|
-
fi
|
|
2952
|
-
cp -r "$SCRIPT_DIR/commands/"* "$TARGET/commands/" 2>/dev/null || true
|
|
2953
|
-
copy_skills "$TARGET" "$PROFILE"
|
|
2954
|
-
cp -r "$SCRIPT_DIR/hooks/"* "$TARGET/hooks/" 2>/dev/null || true
|
|
2955
|
-
cp -r "$SCRIPT_DIR/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
2956
|
-
cp -r "$SCRIPT_DIR/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
2957
|
-
|
|
2958
|
-
local hooks_json
|
|
2959
|
-
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
2960
|
-
if [ ! -f "$TARGET/settings.json" ]; then
|
|
2961
|
-
# Fresh install: generate settings.json from scratch
|
|
2962
|
-
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
2963
|
-
echo "$hooks_json" | python3 -c "
|
|
2964
|
-
import json, sys
|
|
2965
|
-
hooks = json.load(sys.stdin)
|
|
2966
|
-
with open(sys.argv[1]) as f:
|
|
2967
|
-
permissions = json.load(f).get('permissions', {})
|
|
2968
|
-
with open(sys.argv[2], 'w') as f:
|
|
2969
|
-
json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
|
|
2970
|
-
f.write('\n')
|
|
2971
|
-
" "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
2972
|
-
echo " Generated settings.json from hooks/compose.yml"
|
|
2234
|
+
mkdir -p "$git_dir/hooks"
|
|
2235
|
+
local hook_path="$git_dir/hooks/post-merge"
|
|
2236
|
+
if [ -f "$hook_path" ]; then
|
|
2237
|
+
if grep -q "CortexHawk" "$hook_path" 2>/dev/null; then
|
|
2238
|
+
echo " post-merge hook: already installed"
|
|
2973
2239
|
else
|
|
2974
|
-
|
|
2975
|
-
fi
|
|
2976
|
-
else
|
|
2977
|
-
# Merge: preserve user customizations, add new hooks + permissions
|
|
2978
|
-
python3 -c "
|
|
2979
|
-
import json, sys, shutil, os
|
|
2980
|
-
|
|
2981
|
-
raw = sys.stdin.read().strip()
|
|
2982
|
-
hooks = json.loads(raw) if raw else {}
|
|
2983
|
-
|
|
2984
|
-
# Load current settings (tolerate corrupted JSON)
|
|
2985
|
-
try:
|
|
2986
|
-
with open(sys.argv[1]) as f:
|
|
2987
|
-
current = json.load(f)
|
|
2988
|
-
except Exception:
|
|
2989
|
-
backup = sys.argv[1] + '.bak'
|
|
2990
|
-
if os.path.isfile(sys.argv[1]):
|
|
2991
|
-
shutil.copy2(sys.argv[1], backup)
|
|
2992
|
-
print(f' Warning: settings.json corrupted — backed up to {os.path.basename(backup)}', file=sys.stderr)
|
|
2993
|
-
current = {}
|
|
2994
|
-
|
|
2995
|
-
try:
|
|
2996
|
-
with open(sys.argv[2]) as f:
|
|
2997
|
-
source = json.load(f)
|
|
2998
|
-
except Exception:
|
|
2999
|
-
source = {}
|
|
3000
|
-
|
|
3001
|
-
changes = []
|
|
3002
|
-
|
|
3003
|
-
# Merge hooks (regenerate from compose.yml)
|
|
3004
|
-
if hooks and hooks != {}:
|
|
3005
|
-
current['hooks'] = hooks
|
|
3006
|
-
changes.append('hooks regenerated')
|
|
3007
|
-
|
|
3008
|
-
# Merge permissions (union: keep user additions + add new from source)
|
|
3009
|
-
src_perms = source.get('permissions', {})
|
|
3010
|
-
cur_perms = current.get('permissions', {})
|
|
3011
|
-
for key in ('allow', 'deny'):
|
|
3012
|
-
src_list = src_perms.get(key, [])
|
|
3013
|
-
cur_list = cur_perms.get(key, [])
|
|
3014
|
-
added = [p for p in src_list if p not in cur_list]
|
|
3015
|
-
if added:
|
|
3016
|
-
cur_list.extend(added)
|
|
3017
|
-
changes.append(f'{len(added)} new {key} permission(s)')
|
|
3018
|
-
cur_perms[key] = cur_list
|
|
3019
|
-
current['permissions'] = cur_perms
|
|
3020
|
-
|
|
3021
|
-
with open(sys.argv[1], 'w') as f:
|
|
3022
|
-
json.dump(current, f, indent=2)
|
|
3023
|
-
f.write('\n')
|
|
3024
|
-
|
|
3025
|
-
if changes:
|
|
3026
|
-
print(' Merged settings.json: ' + ', '.join(changes))
|
|
3027
|
-
else:
|
|
3028
|
-
print(' settings.json up to date — no merge needed')
|
|
3029
|
-
" "$TARGET/settings.json" "$SCRIPT_DIR/settings.json" <<< "${hooks_json:-}"
|
|
3030
|
-
fi
|
|
3031
|
-
|
|
3032
|
-
PROJECT_ROOT="$(dirname "$TARGET")"
|
|
3033
|
-
if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
|
|
3034
|
-
cp "$SCRIPT_DIR/CLAUDE.md" "$PROJECT_ROOT/CLAUDE.md"
|
|
3035
|
-
else
|
|
3036
|
-
echo "CLAUDE.md already exists — skipping"
|
|
3037
|
-
fi
|
|
3038
|
-
|
|
3039
|
-
# Git workflow config (interactive in --init, defaults otherwise)
|
|
3040
|
-
if [ "$GLOBAL" = false ]; then
|
|
3041
|
-
if [ "$INIT_MODE" = true ]; then
|
|
3042
|
-
source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
|
|
3043
|
-
elif [ ! -f "$TARGET/git-workflow.conf" ]; then
|
|
3044
|
-
# Apply sensible defaults without asking
|
|
3045
|
-
GIT_BRANCHING="direct-main"
|
|
3046
|
-
GIT_COMMIT_CONVENTION="conventional"
|
|
3047
|
-
GIT_PR_PREFERENCE="on-demand"
|
|
3048
|
-
GIT_AUTO_PUSH="after-commit"
|
|
3049
|
-
GIT_WORK_BRANCH=""
|
|
3050
|
-
source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
|
|
2240
|
+
yellow " post-merge hook: skipped (custom hook exists at .git/hooks/post-merge)"
|
|
3051
2241
|
fi
|
|
2242
|
+
return 0
|
|
3052
2243
|
fi
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
update_gitignore "$(dirname "$TARGET")" ".claude"
|
|
3066
|
-
setup_templates "$(dirname "$TARGET")"
|
|
3067
|
-
|
|
3068
|
-
echo ""
|
|
3069
|
-
echo "CortexHawk installed successfully for Claude Code!"
|
|
3070
|
-
echo ""
|
|
3071
|
-
echo " 33 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
|
|
3072
|
-
echo ""
|
|
3073
|
-
do_quickstart
|
|
3074
|
-
echo ""
|
|
3075
|
-
echo " To activate: exit Claude Code (ctrl+c) and relaunch 'claude' in this directory."
|
|
2244
|
+
# Write hook — calls cleanup script in auto mode, never blocks the merge
|
|
2245
|
+
cat > "$hook_path" <<'GITHOOK'
|
|
2246
|
+
#!/bin/bash
|
|
2247
|
+
# post-merge — CortexHawk auto-cleanup after git merge
|
|
2248
|
+
# Installed by CortexHawk. Remove this file to disable.
|
|
2249
|
+
if [ -f ".claude/scripts/post-merge-cleanup.sh" ]; then
|
|
2250
|
+
bash ".claude/scripts/post-merge-cleanup.sh" --auto 2>/dev/null || true
|
|
2251
|
+
fi
|
|
2252
|
+
exit 0
|
|
2253
|
+
GITHOOK
|
|
2254
|
+
chmod +x "$hook_path"
|
|
2255
|
+
green " → native git post-merge hook installed (.git/hooks/post-merge)"
|
|
3076
2256
|
}
|
|
3077
2257
|
|
|
3078
2258
|
# --- install_kimi() ---
|
|
@@ -3330,7 +2510,8 @@ do_test_hooks() {
|
|
|
3330
2510
|
echo ""
|
|
3331
2511
|
|
|
3332
2512
|
local ok=0 fail=0
|
|
3333
|
-
local tmpfile
|
|
2513
|
+
local tmpfile
|
|
2514
|
+
tmpfile=$(mktemp /tmp/.cortexhawk-hooktest-XXXXXX)
|
|
3334
2515
|
echo "test file content" > "$tmpfile"
|
|
3335
2516
|
|
|
3336
2517
|
for hook in "$hooks_dir"/*.sh; do
|
|
@@ -3668,9 +2849,11 @@ do_publish_skill() {
|
|
|
3668
2849
|
# Create GitHub repo
|
|
3669
2850
|
echo ""
|
|
3670
2851
|
echo " Creating repository..."
|
|
2852
|
+
local repo_exists=false
|
|
3671
2853
|
if gh repo view "$gh_user/$repo_name" &>/dev/null; then
|
|
3672
2854
|
echo " Repository already exists: $gh_user/$repo_name"
|
|
3673
2855
|
echo " Pushing latest files..."
|
|
2856
|
+
repo_exists=true
|
|
3674
2857
|
else
|
|
3675
2858
|
gh repo create "$repo_name" --public --description "CortexHawk skill: $skill_desc" --clone=false
|
|
3676
2859
|
fi
|
|
@@ -3694,7 +2877,11 @@ do_publish_skill() {
|
|
|
3694
2877
|
git commit --quiet -m "feat: publish $skill_name skill"
|
|
3695
2878
|
git branch -M main
|
|
3696
2879
|
git remote add origin "https://github.com/$gh_user/$repo_name.git"
|
|
3697
|
-
|
|
2880
|
+
if [ "$repo_exists" = true ]; then
|
|
2881
|
+
git push -u origin main --quiet 2>/dev/null
|
|
2882
|
+
else
|
|
2883
|
+
git push -u origin main --force --quiet 2>/dev/null
|
|
2884
|
+
fi
|
|
3698
2885
|
cd - >/dev/null
|
|
3699
2886
|
|
|
3700
2887
|
# Cleanup
|
|
@@ -3729,7 +2916,8 @@ do_publish_skill() {
|
|
|
3729
2916
|
|
|
3730
2917
|
# --- do_demo() ---
|
|
3731
2918
|
do_demo() {
|
|
3732
|
-
local demo_dir
|
|
2919
|
+
local demo_dir
|
|
2920
|
+
demo_dir=$(mktemp -d /tmp/cortexhawk-demo-XXXXXX)
|
|
3733
2921
|
mkdir -p "$demo_dir"
|
|
3734
2922
|
|
|
3735
2923
|
echo ""
|
|
@@ -3851,6 +3039,13 @@ UTILSJS
|
|
|
3851
3039
|
echo ""
|
|
3852
3040
|
}
|
|
3853
3041
|
|
|
3042
|
+
# --- Source extracted modules ---
|
|
3043
|
+
source "$SCRIPT_DIR/scripts/install-claude.sh"
|
|
3044
|
+
source "$SCRIPT_DIR/scripts/update.sh"
|
|
3045
|
+
source "$SCRIPT_DIR/scripts/snapshot.sh"
|
|
3046
|
+
source "$SCRIPT_DIR/scripts/restore.sh"
|
|
3047
|
+
source "$SCRIPT_DIR/scripts/doctor.sh"
|
|
3048
|
+
|
|
3854
3049
|
# --- Dispatcher ---
|
|
3855
3050
|
if [ "$DEMO_MODE" = true ]; then
|
|
3856
3051
|
do_demo
|
|
@@ -3892,6 +3087,8 @@ elif [ -n "$ENABLE_HOOK" ]; then
|
|
|
3892
3087
|
do_toggle_hook "$ENABLE_HOOK" "enable"
|
|
3893
3088
|
elif [ -n "$DISABLE_HOOK" ]; then
|
|
3894
3089
|
do_toggle_hook "$DISABLE_HOOK" "disable"
|
|
3090
|
+
elif [ "$POST_MERGE_HOOK_MODE" = true ]; then
|
|
3091
|
+
install_git_post_merge_hook "$(pwd)"
|
|
3895
3092
|
elif [ -n "$SEARCH_KEYWORD" ]; then
|
|
3896
3093
|
do_search_skills "$SEARCH_KEYWORD"
|
|
3897
3094
|
elif [ -n "$ADD_SKILL_URL" ]; then
|