cortexhawk 3.3.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/install.sh CHANGED
@@ -34,7 +34,7 @@ green() { printf "\033[32m%s\033[0m\n" "$1"; }
34
34
  yellow() { printf "\033[33m%s\033[0m\n" "$1"; }
35
35
 
36
36
  get_version() {
37
- grep -m1 '## \[' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
37
+ grep -m1 '## \[[0-9]' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
38
38
  }
39
39
 
40
40
  # Portable sed -i (GNU vs BSD)
@@ -173,6 +173,7 @@ CHECK_UPDATE_MODE=false
173
173
  DEMO_MODE=false
174
174
  TRUST_SKILL=false
175
175
  MAX_SNAPSHOTS=10
176
+ POST_MERGE_HOOK_MODE=false
176
177
 
177
178
  # === Component Registry ===
178
179
  # Single source of truth for all CortexHawk components.
@@ -384,6 +385,10 @@ while [ $# -gt 0 ]; do
384
385
  fi
385
386
  shift 2
386
387
  ;;
388
+ --post-merge-hook)
389
+ POST_MERGE_HOOK_MODE=true
390
+ shift
391
+ ;;
387
392
  --version|-v)
388
393
  echo "CortexHawk $(get_version)"
389
394
  exit 0
@@ -916,6 +921,10 @@ cleanup_update() {
916
921
  }
917
922
 
918
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
919
928
  local current_branch
920
929
  current_branch=$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
921
930
  if [ "$current_branch" != "main" ]; then
@@ -1186,431 +1195,6 @@ count_component_files() {
1186
1195
  done
1187
1196
  }
1188
1197
 
1189
- do_update() {
1190
- # 1. Validate target
1191
- if [ "$TARGET_CLI" != "claude" ]; then
1192
- echo "Error: --update is currently supported for Claude Code only"
1193
- echo "For Kimi CLI, re-run: install.sh --target kimi"
1194
- exit 1
1195
- fi
1196
-
1197
- if [ "$GLOBAL" = true ]; then
1198
- TARGET="$HOME/.claude"
1199
- else
1200
- TARGET="$(pwd)/.claude"
1201
- fi
1202
-
1203
- if [ ! -d "$TARGET" ]; then
1204
- echo "Error: no CortexHawk installation found at $TARGET"
1205
- echo "Run install.sh without --update for a fresh install"
1206
- exit 1
1207
- fi
1208
-
1209
- # 2. Read manifest
1210
- local manifest="$TARGET/.cortexhawk-manifest"
1211
- local current_version="unknown"
1212
- local current_profile="all"
1213
- local source_type
1214
- source_type=$(detect_source_type)
1215
-
1216
- if [ -f "$manifest" ]; then
1217
- current_version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1218
- current_profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1219
- local manifest_source
1220
- manifest_source=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1221
- [ -n "$manifest_source" ] && source_type="$manifest_source"
1222
- else
1223
- echo " No manifest found — treating as pre-update installation"
1224
- fi
1225
-
1226
- # 3. Profile override
1227
- local update_profile="$current_profile"
1228
- if [ -n "$PROFILE" ]; then
1229
- update_profile="$PROFILE"
1230
- fi
1231
-
1232
- if [ "$DRY_RUN" = true ]; then
1233
- echo "CortexHawk Dry Run (update)"
1234
- echo "============================"
1235
- else
1236
- echo "CortexHawk Update"
1237
- echo "==================="
1238
- fi
1239
- echo " Current version: $current_version"
1240
- echo " Profile: $update_profile"
1241
- echo " Source: $source_type"
1242
- echo ""
1243
-
1244
- # 3b. Auto-snapshot before update (skip in dry-run)
1245
- if [ "$DRY_RUN" != true ] && [ -f "$manifest" ]; then
1246
- echo "Creating pre-update snapshot..."
1247
- do_snapshot 2>/dev/null
1248
- PRE_UPDATE_SNAP=$(ls -t "$TARGET/.cortexhawk-snapshots"/*.json 2>/dev/null | head -1)
1249
- [ -n "$PRE_UPDATE_SNAP" ] && echo " Saved: $(basename "$PRE_UPDATE_SNAP")"
1250
- echo ""
1251
- fi
1252
-
1253
- # 4. Pull source (skip in dry-run — compare against current source)
1254
- if [ "$DRY_RUN" != true ]; then
1255
- if [ "$source_type" = "git" ]; then
1256
- echo "Updating CortexHawk source via git pull..."
1257
- update_source_git
1258
- else
1259
- echo "Updating CortexHawk source via download..."
1260
- update_source_release
1261
- fi
1262
- echo " Source updated successfully."
1263
- fi
1264
-
1265
- # 5. Compare versions
1266
- local new_version
1267
- new_version=$(get_version)
1268
- echo " New version: $new_version"
1269
- echo ""
1270
-
1271
- if [ "$current_version" = "$new_version" ] && [ "$FORCE_MODE" != true ]; then
1272
- # Same version — check if files actually changed (checksum comparison)
1273
- local files_changed=0
1274
- if [ -f "$manifest" ]; then
1275
- while IFS= read -r line; do
1276
- local fpath fhash
1277
- fpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
1278
- fhash=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
1279
- [ -z "$fpath" ] || [ -z "$fhash" ] && continue
1280
- local source_file="$SCRIPT_DIR/$fpath"
1281
- [ -f "$source_file" ] || continue
1282
- local source_hash
1283
- source_hash=$(compute_checksum "$source_file")
1284
- if [ "$fhash" != "$source_hash" ]; then
1285
- files_changed=$((files_changed + 1))
1286
- fi
1287
- done < <(grep '"sha256:' "$manifest")
1288
- fi
1289
-
1290
- if [ "$files_changed" -eq 0 ] && [ "$DRY_RUN" != true ]; then
1291
- # Still apply install improvements even if no component files changed
1292
- local target_dir_name=".${TARGET_CLI:-claude}"
1293
- update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
1294
- [ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
1295
- if [ ! -f "$TARGET/git-workflow.conf" ]; then
1296
- GIT_BRANCHING="direct-main"
1297
- GIT_COMMIT_CONVENTION="conventional"
1298
- GIT_PR_PREFERENCE="on-demand"
1299
- GIT_AUTO_PUSH="after-commit"
1300
- GIT_WORK_BRANCH=""
1301
- source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$(dirname "$TARGET")" "$TARGET"
1302
- fi
1303
- echo "Already up to date ($new_version). No component files changed."
1304
- cleanup_update
1305
- exit 0
1306
- elif [ "$files_changed" -gt 0 ]; then
1307
- echo " Same version ($new_version) but $files_changed file(s) changed in source."
1308
- fi
1309
- fi
1310
-
1311
- if [ "$DRY_RUN" = true ]; then
1312
- echo " Comparing source $new_version vs installed $current_version"
1313
- elif [ "$current_version" = "$new_version" ]; then
1314
- echo " Syncing changed files ($new_version)..."
1315
- else
1316
- echo " Updating $current_version -> $new_version"
1317
- fi
1318
- echo ""
1319
-
1320
- # Set up profile file for skill filtering
1321
- if [ -n "$update_profile" ] && [ "$update_profile" != "all" ]; then
1322
- PROFILE_FILE="$SCRIPT_DIR/profiles/${update_profile}.json"
1323
- if [ ! -f "$PROFILE_FILE" ]; then
1324
- echo " Warning: profile '$update_profile' not found in source — installing all skills"
1325
- update_profile="all"
1326
- PROFILE_FILE=""
1327
- fi
1328
- fi
1329
-
1330
- # 6. Reset counters and sync components
1331
- SYNC_ADDED=0
1332
- SYNC_UPDATED=0
1333
- SYNC_UNCHANGED=0
1334
- SYNC_SKIPPED=0
1335
- SYNC_CONFLICTS=0
1336
-
1337
- sync_all_components "$update_profile"
1338
-
1339
- # 6b. Sync agent personas from project root
1340
- local project_root
1341
- project_root="$(dirname "$TARGET")"
1342
- if [ -d "$project_root/.cortexhawk-agents" ] && [ "$DRY_RUN" != true ]; then
1343
- cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
1344
- local pc
1345
- pc=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
1346
- [ "$pc" -gt 0 ] && echo " Synced $pc agent persona(s) from .cortexhawk-agents/"
1347
- fi
1348
-
1349
- # 7. Detect removed upstream files
1350
- if [ -f "$manifest" ]; then
1351
- while IFS= read -r line; do
1352
- local file_relpath
1353
- file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/')
1354
- [ -z "$file_relpath" ] && continue
1355
- if [ ! -f "$SCRIPT_DIR/$file_relpath" ] && [ -f "$TARGET/$file_relpath" ]; then
1356
- echo " Warning: $file_relpath was removed upstream (kept locally)"
1357
- fi
1358
- done < <(grep '"sha256:' "$manifest")
1359
- fi
1360
-
1361
- if [ "$DRY_RUN" != true ]; then
1362
- # Make executable components executable
1363
- for entry in "${COMPONENTS[@]}"; do
1364
- IFS=':' read -r name exec_flag <<< "$entry"
1365
- [ "$exec_flag" = "yes" ] && chmod +x "$TARGET/$name/"*.sh 2>/dev/null || true
1366
- done
1367
-
1368
- # 7b. Regenerate settings.json hooks + merge new permissions from compose.yml
1369
- if [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
1370
- local hooks_json
1371
- hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
1372
- if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
1373
- echo "$hooks_json" | python3 -c "
1374
- import json, sys
1375
- hooks = json.load(sys.stdin)
1376
- current = {}
1377
- try:
1378
- with open(sys.argv[1]) as f:
1379
- current = json.load(f)
1380
- except:
1381
- pass
1382
- current['hooks'] = hooks
1383
- # Merge new permissions from source
1384
- try:
1385
- with open(sys.argv[2]) as f:
1386
- src_perms = json.load(f).get('permissions', {})
1387
- cur_perms = current.get('permissions', {})
1388
- for key in ('allow', 'deny'):
1389
- src_list = src_perms.get(key, [])
1390
- cur_list = cur_perms.get(key, [])
1391
- added = [p for p in src_list if p not in cur_list]
1392
- if added:
1393
- cur_list.extend(added)
1394
- cur_perms[key] = cur_list
1395
- current['permissions'] = cur_perms
1396
- except:
1397
- pass
1398
- with open(sys.argv[1], 'w') as f:
1399
- json.dump(current, f, indent=2)
1400
- f.write('\n')
1401
- " "$TARGET/settings.json" "$SCRIPT_DIR/settings.json"
1402
- echo " Regenerated settings.json hooks from compose.yml"
1403
- fi
1404
- fi
1405
-
1406
- # 8. Write new manifest
1407
- write_manifest "$TARGET" "$update_profile" "$TARGET_CLI" true
1408
-
1409
- # 9. Run audit
1410
- run_audit "$(dirname "$TARGET")"
1411
-
1412
- # 10. Apply install improvements (gitignore, git-workflow defaults)
1413
- local target_dir_name=".${TARGET_CLI:-claude}"
1414
- update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
1415
- [ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
1416
- setup_templates "$(dirname "$TARGET")"
1417
-
1418
- if [ ! -f "$TARGET/git-workflow.conf" ]; then
1419
- GIT_BRANCHING="direct-main"
1420
- GIT_COMMIT_CONVENTION="conventional"
1421
- GIT_PR_PREFERENCE="on-demand"
1422
- GIT_AUTO_PUSH="after-commit"
1423
- GIT_WORK_BRANCH=""
1424
- source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$(dirname "$TARGET")" "$TARGET"
1425
- fi
1426
- fi
1427
-
1428
- # 10. Print summary
1429
- echo ""
1430
- if [ "$DRY_RUN" = true ]; then
1431
- echo "Dry run summary:"
1432
- echo " Would add: $SYNC_ADDED"
1433
- echo " Would update: $SYNC_UPDATED"
1434
- echo " Unchanged: $SYNC_UNCHANGED"
1435
- echo " Would skip: $SYNC_SKIPPED"
1436
- echo " Conflicts: $SYNC_CONFLICTS"
1437
- echo ""
1438
- echo "No files were modified (dry run)."
1439
- else
1440
- echo "Update complete: $current_version -> $new_version"
1441
- echo " Added: $SYNC_ADDED"
1442
- echo " Updated: $SYNC_UPDATED"
1443
- echo " Unchanged: $SYNC_UNCHANGED"
1444
- echo " Skipped: $SYNC_SKIPPED"
1445
- echo " Conflicts: $SYNC_CONFLICTS"
1446
- if [ -n "${PRE_UPDATE_SNAP:-}" ]; then
1447
- echo " Rollback: install.sh --restore $PRE_UPDATE_SNAP"
1448
- fi
1449
- echo ""
1450
- echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
1451
- fi
1452
-
1453
- cleanup_update
1454
- }
1455
-
1456
- # --- rotate_snapshots() ---
1457
- # Keeps only the N most recent snapshots, deletes the rest
1458
- rotate_snapshots() {
1459
- local snap_dir="$1"
1460
- local snaps
1461
- snaps=$(find "$snap_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
1462
- [ -z "$snaps" ] && return 0
1463
- local count
1464
- count=$(echo "$snaps" | wc -l | tr -d ' ')
1465
- if [ "$count" -gt "$MAX_SNAPSHOTS" ]; then
1466
- local to_delete=$((count - MAX_SNAPSHOTS))
1467
- ls -1t "$snap_dir"/*.json | tail -n "$to_delete" | while read -r old_snap; do
1468
- rm -f "$old_snap"
1469
- done
1470
- echo " Rotated: removed $to_delete old snapshot(s), keeping $MAX_SNAPSHOTS"
1471
- fi
1472
- }
1473
-
1474
- # --- do_snapshot() ---
1475
- do_snapshot() {
1476
- if [ "$GLOBAL" = true ]; then
1477
- TARGET="$HOME/.claude"
1478
- else
1479
- TARGET="$(pwd)/.claude"
1480
- fi
1481
-
1482
- local manifest="$TARGET/.cortexhawk-manifest"
1483
- if [ ! -f "$manifest" ]; then
1484
- echo "Error: no CortexHawk manifest found at $manifest"
1485
- echo "Run install.sh first to create an installation"
1486
- exit 1
1487
- fi
1488
-
1489
- # Read manifest metadata
1490
- local version
1491
- version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1492
- local profile
1493
- profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1494
- local source_type
1495
- source_type=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1496
- local source_url
1497
- source_url=$(grep '"source_url"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1498
- local source_path
1499
- source_path=$(grep '"source_path"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1500
-
1501
- # Read settings.json
1502
- local settings_json="null"
1503
- if [ -f "$TARGET/settings.json" ]; then
1504
- settings_json=$(cat "$TARGET/settings.json")
1505
- fi
1506
-
1507
- # Read git-workflow.conf
1508
- local git_branching="" git_commit="" git_pr="" git_push=""
1509
- if [ -f "$TARGET/git-workflow.conf" ]; then
1510
- git_branching=$(grep '^BRANCHING=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1511
- git_commit=$(grep '^COMMIT_CONVENTION=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1512
- git_pr=$(grep '^PR_PREFERENCE=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1513
- git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1514
- fi
1515
-
1516
- # Read custom profile if applicable (use PROFILE_FILE from current run, never glob /tmp/)
1517
- local profile_def="null"
1518
- if [ -n "$PROFILE_FILE" ] && [ -f "$PROFILE_FILE" ]; then
1519
- profile_def=$(cat "$PROFILE_FILE")
1520
- fi
1521
-
1522
- # Build files checksums from manifest
1523
- local files_json
1524
- files_json=$(sed -n '/"files"/,/^ }/p' "$manifest" | sed '1d;$d')
1525
-
1526
- # Collect file contents (base64 encoded for binary safety)
1527
- local git_workflow_content="" claude_md_content=""
1528
- if [ -f "$TARGET/git-workflow.conf" ]; then
1529
- git_workflow_content=$(base64 < "$TARGET/git-workflow.conf" | tr -d '\n')
1530
- fi
1531
- if [ -f "$TARGET/../CLAUDE.md" ]; then
1532
- claude_md_content=$(base64 < "$TARGET/../CLAUDE.md" | tr -d '\n')
1533
- fi
1534
-
1535
- # Generate snapshot
1536
- local now
1537
- now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1538
- local snap_name
1539
- snap_name=$(date -u +"%Y-%m-%d-%H%M%S")
1540
- local snap_dir="$TARGET/.cortexhawk-snapshots"
1541
- local snap_file="$snap_dir/${snap_name}.json"
1542
-
1543
- mkdir -p "$snap_dir"
1544
-
1545
- # Write snapshot JSON
1546
- printf '{\n' > "$snap_file"
1547
- printf ' "snapshot_version": "2",\n' >> "$snap_file"
1548
- printf ' "snapshot_date": "%s",\n' "$now" >> "$snap_file"
1549
- printf ' "cortexhawk_version": "%s",\n' "$version" >> "$snap_file"
1550
- printf ' "target": "claude",\n' >> "$snap_file"
1551
- printf ' "profile": "%s",\n' "$profile" >> "$snap_file"
1552
- printf ' "profile_definition": %s,\n' "$profile_def" >> "$snap_file"
1553
- printf ' "source": "%s",\n' "$source_type" >> "$snap_file"
1554
- printf ' "source_url": "%s",\n' "$source_url" >> "$snap_file"
1555
- printf ' "source_path": "%s",\n' "$source_path" >> "$snap_file"
1556
- printf ' "settings": %s,\n' "$settings_json" >> "$snap_file"
1557
- printf ' "git_workflow": {\n' >> "$snap_file"
1558
- printf ' "BRANCHING": "%s",\n' "$git_branching" >> "$snap_file"
1559
- printf ' "COMMIT_CONVENTION": "%s",\n' "$git_commit" >> "$snap_file"
1560
- printf ' "PR_PREFERENCE": "%s",\n' "$git_pr" >> "$snap_file"
1561
- printf ' "AUTO_PUSH": "%s"\n' "$git_push" >> "$snap_file"
1562
- printf ' },\n' >> "$snap_file"
1563
- printf ' "files": {\n' >> "$snap_file"
1564
- printf '%s\n' "$files_json" >> "$snap_file"
1565
- printf ' },\n' >> "$snap_file"
1566
- printf ' "file_contents": {\n' >> "$snap_file"
1567
- [ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
1568
- [ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
1569
- # Remove trailing comma from last entry
1570
- sed_inplace '$ s/,$//' "$snap_file"
1571
- printf ' }\n' >> "$snap_file"
1572
- printf '}\n' >> "$snap_file"
1573
-
1574
- echo "CortexHawk Snapshot"
1575
- echo "====================="
1576
- echo " Version: $version"
1577
- echo " Profile: $profile"
1578
- echo " Target: $TARGET"
1579
- echo " Saved to: $snap_file"
1580
-
1581
- # Create portable archive if requested
1582
- if [ "$PORTABLE_MODE" = true ]; then
1583
- local archive_name="${snap_name}.tar.gz"
1584
- local archive_path="$snap_dir/$archive_name"
1585
- local tmp_dir
1586
- tmp_dir=$(mktemp -d)
1587
-
1588
- # Copy snapshot and files to temp structure
1589
- cp "$snap_file" "$tmp_dir/snapshot.json"
1590
- mkdir -p "$tmp_dir/files"
1591
- cp -r "$TARGET/agents" "$tmp_dir/files/" 2>/dev/null || true
1592
- cp -r "$TARGET/commands" "$tmp_dir/files/" 2>/dev/null || true
1593
- cp -r "$TARGET/skills" "$tmp_dir/files/" 2>/dev/null || true
1594
- cp -r "$TARGET/hooks" "$tmp_dir/files/" 2>/dev/null || true
1595
- cp -r "$TARGET/modes" "$tmp_dir/files/" 2>/dev/null || true
1596
- cp -r "$TARGET/mcp" "$tmp_dir/files/" 2>/dev/null || true
1597
- cp "$TARGET/settings.json" "$tmp_dir/files/" 2>/dev/null || true
1598
- cp "$TARGET/git-workflow.conf" "$tmp_dir/files/" 2>/dev/null || true
1599
-
1600
- # Create archive
1601
- tar -czf "$archive_path" -C "$tmp_dir" .
1602
- rm -rf "$tmp_dir"
1603
-
1604
- echo " Archive: $archive_path"
1605
- echo ""
1606
- echo "Restore with: install.sh --restore $archive_path"
1607
- else
1608
- rotate_snapshots "$snap_dir"
1609
- echo ""
1610
- echo "Restore with: install.sh --restore $snap_file"
1611
- fi
1612
- }
1613
-
1614
1198
  # --- do_snapshots_list() ---
1615
1199
  do_snapshots_list() {
1616
1200
  if [ "$GLOBAL" = true ]; then
@@ -1960,358 +1544,6 @@ do_export_team() {
1960
1544
  echo "Share this file with your team. Install with: install.sh --team"
1961
1545
  }
1962
1546
 
1963
- # --- do_restore() ---
1964
- do_restore() {
1965
- local snap_file="$1"
1966
- local archive_tmp=""
1967
- local portable_files=""
1968
-
1969
- if [ -z "$snap_file" ] || [ ! -f "$snap_file" ]; then
1970
- echo "Error: snapshot file not found: $snap_file"
1971
- exit 1
1972
- fi
1973
-
1974
- # Handle portable archive (.tar.gz)
1975
- if [[ "$snap_file" == *.tar.gz ]]; then
1976
- archive_tmp=$(mktemp -d)
1977
- tar -xzf "$snap_file" -C "$archive_tmp"
1978
- snap_file="$archive_tmp/snapshot.json"
1979
- portable_files="$archive_tmp/files"
1980
- if [ ! -f "$snap_file" ]; then
1981
- echo "Error: invalid archive — snapshot.json not found"
1982
- rm -rf "$archive_tmp"
1983
- exit 1
1984
- fi
1985
- echo "Extracting portable archive..."
1986
- fi
1987
-
1988
- # Extract metadata from snapshot
1989
- local snap_version
1990
- snap_version=$(grep '"cortexhawk_version"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1991
- local snap_profile
1992
- snap_profile=$(grep '"profile"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1993
- local snap_source
1994
- snap_source=$(grep '"source"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1995
- local snap_source_url
1996
- snap_source_url=$(grep '"source_url"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1997
- local snap_source_path
1998
- snap_source_path=$(grep '"source_path"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1999
-
2000
- echo "CortexHawk Restore"
2001
- echo "====================="
2002
- echo " Snapshot version: $snap_version"
2003
- echo " Profile: $snap_profile"
2004
- echo " Source: $snap_source"
2005
- echo ""
2006
-
2007
- # Determine CortexHawk source
2008
- local restore_source="$SCRIPT_DIR"
2009
- if [ "$snap_source" = "git" ] && [ -n "$snap_source_path" ] && [ -d "$snap_source_path" ]; then
2010
- restore_source="$snap_source_path"
2011
- fi
2012
- if [ ! -d "$restore_source/agents" ]; then
2013
- echo "Error: CortexHawk source not found at $restore_source"
2014
- echo "Ensure the CortexHawk repo is available or set CORTEXHAWK_REPO"
2015
- exit 1
2016
- fi
2017
-
2018
- # Warn if source version differs from snapshot
2019
- local current_version
2020
- current_version=$(get_version)
2021
- if [ "$snap_version" != "$current_version" ]; then
2022
- echo " Warning: snapshot is v$snap_version but source is v$current_version"
2023
- echo " Some file checksums may not match"
2024
- echo ""
2025
- fi
2026
-
2027
- # Set profile for reinstall
2028
- if [ -n "$snap_profile" ] && [ "$snap_profile" != "all" ]; then
2029
- PROFILE="$snap_profile"
2030
- PROFILE_FILE="$restore_source/profiles/${snap_profile}.json"
2031
- if [ ! -f "$PROFILE_FILE" ]; then
2032
- echo " Warning: profile '$snap_profile' not found — installing all skills"
2033
- PROFILE=""
2034
- PROFILE_FILE=""
2035
- fi
2036
- fi
2037
-
2038
- # Determine target
2039
- if [ "$GLOBAL" = true ]; then
2040
- TARGET="$HOME/.claude"
2041
- else
2042
- TARGET="$(pwd)/.claude"
2043
- fi
2044
-
2045
- # Save the original SCRIPT_DIR, use snapshot source
2046
- local orig_script_dir="$SCRIPT_DIR"
2047
- SCRIPT_DIR="$restore_source"
2048
-
2049
- # Reinstall using the standard flow
2050
- echo "Reinstalling CortexHawk components..."
2051
-
2052
- # Use portable archive files if available, otherwise use source repo
2053
- if [ -n "$portable_files" ] && [ -d "$portable_files" ]; then
2054
- echo " Using files from portable archive..."
2055
- copy_all_components "$portable_files" "$TARGET" ""
2056
- else
2057
- copy_all_components "$SCRIPT_DIR" "$TARGET" "$PROFILE"
2058
- fi
2059
-
2060
- # Restore settings.json from snapshot
2061
- if command -v python3 >/dev/null 2>&1; then
2062
- python3 -c "
2063
- import json, sys
2064
- with open(sys.argv[1]) as f:
2065
- snap = json.load(f)
2066
- settings = snap.get('settings')
2067
- if settings is not None:
2068
- with open(sys.argv[2], 'w') as f:
2069
- json.dump(settings, f, indent=2)
2070
- f.write('\n')
2071
- print(' Restored settings.json')
2072
- " "$snap_file" "$TARGET/settings.json"
2073
- else
2074
- echo " Warning: python3 not found — settings.json not restored from snapshot"
2075
- fi
2076
-
2077
- # Restore git-workflow.conf from snapshot
2078
- local branching commit_conv pr_pref auto_push
2079
- branching=$(grep '"BRANCHING"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
2080
- commit_conv=$(grep '"COMMIT_CONVENTION"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
2081
- pr_pref=$(grep '"PR_PREFERENCE"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
2082
- auto_push=$(grep '"AUTO_PUSH"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
2083
-
2084
- if [ -n "$branching" ] || [ -n "$commit_conv" ] || [ -n "$pr_pref" ] || [ -n "$auto_push" ]; then
2085
- {
2086
- echo "BRANCHING=$branching"
2087
- echo "COMMIT_CONVENTION=$commit_conv"
2088
- echo "PR_PREFERENCE=$pr_pref"
2089
- echo "AUTO_PUSH=$auto_push"
2090
- } > "$TARGET/git-workflow.conf"
2091
- echo " Restored git-workflow.conf (from git_workflow keys)"
2092
- fi
2093
-
2094
- # Restore file_contents (snapshot v2) — overwrites git-workflow.conf if present
2095
- if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
2096
- python3 -c "
2097
- import json, base64, sys, os
2098
- snap_file, target_dir = sys.argv[1], sys.argv[2]
2099
- with open(snap_file) as f:
2100
- snap = json.load(f)
2101
- contents = snap.get('file_contents', {})
2102
- for filename, b64data in contents.items():
2103
- try:
2104
- data = base64.b64decode(b64data).decode('utf-8')
2105
- if filename == 'CLAUDE.md':
2106
- target_path = os.path.dirname(target_dir) + '/CLAUDE.md'
2107
- else:
2108
- target_path = target_dir + '/' + filename
2109
- os.makedirs(os.path.dirname(target_path), exist_ok=True)
2110
- with open(target_path, 'w') as f:
2111
- f.write(data)
2112
- print(f' Restored {filename} (from file_contents)')
2113
- except Exception as e:
2114
- print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
2115
- " "$snap_file" "$TARGET"
2116
- fi
2117
-
2118
- # Write new manifest
2119
- write_manifest "$TARGET" "$PROFILE" "claude" false
2120
-
2121
- # Verify checksums against snapshot
2122
- local verified=0 mismatched=0 missing=0
2123
- while IFS= read -r line; do
2124
- local file_relpath
2125
- file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
2126
- local expected_checksum
2127
- expected_checksum=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
2128
- [ -z "$file_relpath" ] || [ -z "$expected_checksum" ] && continue
2129
-
2130
- local target_file="$TARGET/$file_relpath"
2131
- if [ ! -f "$target_file" ]; then
2132
- missing=$((missing + 1))
2133
- else
2134
- local actual_checksum
2135
- actual_checksum=$(compute_checksum "$target_file")
2136
- if [ "$actual_checksum" = "$expected_checksum" ]; then
2137
- verified=$((verified + 1))
2138
- else
2139
- mismatched=$((mismatched + 1))
2140
- fi
2141
- fi
2142
- done < <(grep '"sha256:' "$snap_file")
2143
-
2144
- SCRIPT_DIR="$orig_script_dir"
2145
-
2146
- echo ""
2147
- echo "Restore complete"
2148
- echo " Verified: $verified files match snapshot checksums"
2149
- [ "$mismatched" -gt 0 ] && echo " Mismatched: $mismatched files differ (source version may differ from snapshot)"
2150
- [ "$missing" -gt 0 ] && echo " Missing: $missing files not found in source"
2151
- echo ""
2152
- echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
2153
-
2154
- # Cleanup temp dir from portable archive
2155
- [ -n "$archive_tmp" ] && rm -rf "$archive_tmp"
2156
- }
2157
-
2158
- # --- do_doctor() ---
2159
- # Diagnose installation health
2160
- do_doctor() {
2161
- if [ "$GLOBAL" = true ]; then
2162
- TARGET="$HOME/.claude"
2163
- else
2164
- TARGET="$(pwd)/.claude"
2165
- fi
2166
-
2167
- local ok=0 warn=0 err=0
2168
- _doc_ok() { echo " [OK] $1"; ok=$((ok+1)); }
2169
- _doc_warn() { echo " [WARN] $1"; warn=$((warn+1)); }
2170
- _doc_err() { echo " [ERR] $1"; err=$((err+1)); }
2171
-
2172
- # Header
2173
- local version="" profile="" target_cli_name=""
2174
- if [ -f "$TARGET/.cortexhawk-manifest" ]; then
2175
- version=$(grep -o '"version": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
2176
- profile=$(grep -o '"profile": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
2177
- target_cli_name=$(grep -o '"target": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
2178
- fi
2179
- echo "CortexHawk Doctor"
2180
- echo "==================="
2181
- echo " Installation: $TARGET"
2182
- echo " Version: ${version:-unknown}"
2183
- echo " Profile: ${profile:-unknown}"
2184
- echo " Target: ${target_cli_name:-claude}"
2185
- echo ""
2186
- echo "Checks:"
2187
-
2188
- # 1. Manifest
2189
- if [ -f "$TARGET/.cortexhawk-manifest" ]; then
2190
- _doc_ok "Manifest present"
2191
- else
2192
- _doc_err "Manifest missing — run install.sh to create an installation"
2193
- fi
2194
-
2195
- # 2. settings.json valid JSON
2196
- if [ -f "$TARGET/settings.json" ]; then
2197
- if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$TARGET/settings.json" 2>/dev/null; then
2198
- _doc_ok "settings.json valid JSON"
2199
- else
2200
- _doc_err "settings.json invalid JSON"
2201
- fi
2202
- else
2203
- _doc_warn "settings.json not found"
2204
- fi
2205
-
2206
- # 3. Component counts (compare installed vs source)
2207
- for comp in agents commands modes; do
2208
- local installed=0 source_count=0
2209
- installed=$(find "$TARGET/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
2210
- source_count=$(find "$SCRIPT_DIR/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
2211
- if [ "$installed" -eq "$source_count" ] 2>/dev/null; then
2212
- _doc_ok "$installed/$source_count $comp installed"
2213
- elif [ "$installed" -gt 0 ] 2>/dev/null; then
2214
- _doc_warn "$installed/$source_count $comp installed"
2215
- else
2216
- _doc_err "0/$source_count $comp installed"
2217
- fi
2218
- done
2219
-
2220
- # 4. Skills (profile-dependent, just count what's there)
2221
- local skills_installed=0 skills_source=0
2222
- skills_installed=$(find "$TARGET/skills" -name "*.md" -type f 2>/dev/null | wc -l)
2223
- skills_source=$(find "$SCRIPT_DIR/skills" -name "*.md" -type f 2>/dev/null | wc -l)
2224
- if [ "$skills_installed" -gt 0 ] 2>/dev/null; then
2225
- _doc_ok "$skills_installed/$skills_source skills installed (profile: ${profile:-all})"
2226
- else
2227
- _doc_err "No skills installed"
2228
- fi
2229
-
2230
- # 5. Hooks executable
2231
- local hooks_ok=0 hooks_total=0
2232
- for hook in "$TARGET/hooks/"*.sh; do
2233
- [ -f "$hook" ] || continue
2234
- hooks_total=$((hooks_total+1))
2235
- if [ -x "$hook" ]; then
2236
- hooks_ok=$((hooks_ok+1))
2237
- else
2238
- _doc_warn "Hook not executable: $(basename "$hook")"
2239
- fi
2240
- done
2241
- if [ "$hooks_total" -gt 0 ]; then
2242
- if [ "$hooks_ok" -eq "$hooks_total" ]; then
2243
- _doc_ok "$hooks_ok/$hooks_total hooks executable"
2244
- fi
2245
- else
2246
- _doc_warn "No hooks found"
2247
- fi
2248
-
2249
- # 6. compose.yml vs settings.json coherence
2250
- if [ -f "$TARGET/../hooks/compose.yml" ] || [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
2251
- _doc_ok "compose.yml present"
2252
- fi
2253
-
2254
- # 7. MCP configs
2255
- if [ -d "$TARGET/mcp" ] && [ "$(find "$TARGET/mcp" -type f 2>/dev/null | wc -l)" -gt 0 ]; then
2256
- _doc_ok "MCP configs present"
2257
- elif [ -d "$TARGET/mcp" ]; then
2258
- _doc_warn "MCP directory exists but empty"
2259
- fi
2260
-
2261
- # 8. docs/ workspace
2262
- local project_root
2263
- project_root="$(dirname "$TARGET")"
2264
- if [ -d "$project_root/docs" ]; then
2265
- _doc_ok "docs/ workspace exists"
2266
- else
2267
- _doc_warn "docs/ workspace missing"
2268
- fi
2269
-
2270
- # 9. Broken symlinks in docs/plans/
2271
- local broken=0
2272
- if [ -d "$project_root/docs/plans" ]; then
2273
- while IFS= read -r link; do
2274
- [ -z "$link" ] && continue
2275
- _doc_warn "Broken symlink: $link"
2276
- broken=$((broken+1))
2277
- done < <(find "$project_root/docs/plans" -type l ! -exec test -e {} \; -print 2>/dev/null)
2278
- [ "$broken" -eq 0 ] && _doc_ok "No broken symlinks in docs/plans/"
2279
- fi
2280
-
2281
- # 10. git-workflow.conf
2282
- if [ -f "$TARGET/git-workflow.conf" ]; then
2283
- _doc_ok "git-workflow.conf present"
2284
- else
2285
- _doc_warn "git-workflow.conf not found (run --init to configure)"
2286
- fi
2287
-
2288
- # 11. CLAUDE.md at project root
2289
- if [ -f "$project_root/CLAUDE.md" ]; then
2290
- _doc_ok "CLAUDE.md present at project root"
2291
- else
2292
- _doc_warn "CLAUDE.md not found at project root"
2293
- fi
2294
-
2295
- # 12. Version match source vs manifest
2296
- if [ -n "$version" ]; then
2297
- local source_version
2298
- source_version=$(get_version)
2299
- if [ "$version" = "$source_version" ]; then
2300
- _doc_ok "Version match: source $source_version = manifest $version"
2301
- else
2302
- _doc_warn "Version mismatch: source $source_version != manifest $version (run --update)"
2303
- fi
2304
- fi
2305
-
2306
- # Summary
2307
- echo ""
2308
- echo "Summary: $ok OK, $warn WARN, $err ERR"
2309
-
2310
- # Exit code: 1 if any errors
2311
- [ "$err" -gt 0 ] && exit 1
2312
- return 0
2313
- }
2314
-
2315
1547
  # --- do_uninstall() ---
2316
1548
  # Remove CortexHawk installation cleanly
2317
1549
  do_uninstall() {
@@ -2990,161 +2222,37 @@ do_team_install() {
2990
2222
  echo "Team install complete."
2991
2223
  }
2992
2224
 
2993
- # --- install_claude() ---
2994
- install_claude() {
2995
- if [ "$GLOBAL" = true ]; then
2996
- TARGET="$HOME/.claude"
2997
- else
2998
- TARGET="$(pwd)/.claude"
2999
- fi
3000
-
3001
- if [ "$DRY_RUN" = true ]; then
3002
- echo "CortexHawk Dry Run (install)"
3003
- echo "=============================="
3004
- echo " Target: $TARGET"
3005
- echo " Profile: ${PROFILE:-all}"
3006
- echo ""
3007
- echo "Would install:"
3008
- count_component_files "$SCRIPT_DIR"
3009
- echo " settings.json"
3010
- [ ! -f "$(pwd)/CLAUDE.md" ] && echo " CLAUDE.md"
3011
- echo ""
3012
- echo "No files were modified (dry run)."
3013
- 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
3014
2233
  fi
3015
-
3016
- echo "Installing for Claude Code to project: $TARGET"
3017
-
3018
- copy_all_components "$SCRIPT_DIR" "$TARGET" "$PROFILE"
3019
-
3020
- # Copy agent personas from project root if present
3021
- local project_root
3022
- project_root="$(dirname "$TARGET")"
3023
- if [ -d "$project_root/.cortexhawk-agents" ]; then
3024
- cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
3025
- local persona_count
3026
- persona_count=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
3027
- [ "$persona_count" -gt 0 ] && echo " Loaded $persona_count agent persona(s) from .cortexhawk-agents/"
3028
- fi
3029
-
3030
- local hooks_json
3031
- hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
3032
- if [ ! -f "$TARGET/settings.json" ]; then
3033
- # Fresh install: generate settings.json from scratch
3034
- if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
3035
- echo "$hooks_json" | python3 -c "
3036
- import json, sys
3037
- hooks = json.load(sys.stdin)
3038
- with open(sys.argv[1]) as f:
3039
- permissions = json.load(f).get('permissions', {})
3040
- with open(sys.argv[2], 'w') as f:
3041
- json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
3042
- f.write('\n')
3043
- " "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
3044
- 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"
3045
2239
  else
3046
- cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
3047
- fi
3048
- else
3049
- # Merge: preserve user customizations, add new hooks + permissions
3050
- python3 -c "
3051
- import json, sys, shutil, os
3052
-
3053
- raw = sys.stdin.read().strip()
3054
- hooks = json.loads(raw) if raw else {}
3055
-
3056
- # Load current settings (tolerate corrupted JSON)
3057
- try:
3058
- with open(sys.argv[1]) as f:
3059
- current = json.load(f)
3060
- except Exception:
3061
- backup = sys.argv[1] + '.bak'
3062
- if os.path.isfile(sys.argv[1]):
3063
- shutil.copy2(sys.argv[1], backup)
3064
- print(f' Warning: settings.json corrupted — backed up to {os.path.basename(backup)}', file=sys.stderr)
3065
- current = {}
3066
-
3067
- try:
3068
- with open(sys.argv[2]) as f:
3069
- source = json.load(f)
3070
- except Exception:
3071
- source = {}
3072
-
3073
- changes = []
3074
-
3075
- # Merge hooks (regenerate from compose.yml)
3076
- if hooks and hooks != {}:
3077
- current['hooks'] = hooks
3078
- changes.append('hooks regenerated')
3079
-
3080
- # Merge permissions (union: keep user additions + add new from source)
3081
- src_perms = source.get('permissions', {})
3082
- cur_perms = current.get('permissions', {})
3083
- for key in ('allow', 'deny'):
3084
- src_list = src_perms.get(key, [])
3085
- cur_list = cur_perms.get(key, [])
3086
- added = [p for p in src_list if p not in cur_list]
3087
- if added:
3088
- cur_list.extend(added)
3089
- changes.append(f'{len(added)} new {key} permission(s)')
3090
- cur_perms[key] = cur_list
3091
- current['permissions'] = cur_perms
3092
-
3093
- with open(sys.argv[1], 'w') as f:
3094
- json.dump(current, f, indent=2)
3095
- f.write('\n')
3096
-
3097
- if changes:
3098
- print(' Merged settings.json: ' + ', '.join(changes))
3099
- else:
3100
- print(' settings.json up to date — no merge needed')
3101
- " "$TARGET/settings.json" "$SCRIPT_DIR/settings.json" <<< "${hooks_json:-}"
3102
- fi
3103
-
3104
- PROJECT_ROOT="$(dirname "$TARGET")"
3105
- if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
3106
- cp "$SCRIPT_DIR/CLAUDE.md" "$PROJECT_ROOT/CLAUDE.md"
3107
- else
3108
- echo "CLAUDE.md already exists — skipping"
3109
- fi
3110
-
3111
- # Git workflow config (interactive in --init, defaults otherwise)
3112
- if [ "$GLOBAL" = false ]; then
3113
- if [ "$INIT_MODE" = true ]; then
3114
- source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
3115
- elif [ ! -f "$TARGET/git-workflow.conf" ]; then
3116
- # Apply sensible defaults without asking
3117
- GIT_BRANCHING="direct-main"
3118
- GIT_COMMIT_CONVENTION="conventional"
3119
- GIT_PR_PREFERENCE="on-demand"
3120
- GIT_AUTO_PUSH="after-commit"
3121
- GIT_WORK_BRANCH=""
3122
- 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)"
3123
2241
  fi
2242
+ return 0
3124
2243
  fi
3125
-
3126
- chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
3127
-
3128
- # Write manifest for future updates
3129
- write_manifest "$TARGET" "$PROFILE" "claude" false
3130
-
3131
- # Create docs/ workspace for agent outputs (local only)
3132
- if [ "$GLOBAL" = false ]; then
3133
- create_docs_workspace "$(dirname "$TARGET")"
3134
- fi
3135
-
3136
- run_audit "$(dirname "$TARGET")"
3137
- update_gitignore "$(dirname "$TARGET")" ".claude"
3138
- setup_templates "$(dirname "$TARGET")"
3139
-
3140
- echo ""
3141
- echo "CortexHawk installed successfully for Claude Code!"
3142
- echo ""
3143
- echo " 33 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
3144
- echo ""
3145
- do_quickstart
3146
- echo ""
3147
- 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)"
3148
2256
  }
3149
2257
 
3150
2258
  # --- install_kimi() ---
@@ -3931,6 +3039,13 @@ UTILSJS
3931
3039
  echo ""
3932
3040
  }
3933
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
+
3934
3049
  # --- Dispatcher ---
3935
3050
  if [ "$DEMO_MODE" = true ]; then
3936
3051
  do_demo
@@ -3972,6 +3087,8 @@ elif [ -n "$ENABLE_HOOK" ]; then
3972
3087
  do_toggle_hook "$ENABLE_HOOK" "enable"
3973
3088
  elif [ -n "$DISABLE_HOOK" ]; then
3974
3089
  do_toggle_hook "$DISABLE_HOOK" "disable"
3090
+ elif [ "$POST_MERGE_HOOK_MODE" = true ]; then
3091
+ install_git_post_merge_hook "$(pwd)"
3975
3092
  elif [ -n "$SEARCH_KEYWORD" ]; then
3976
3093
  do_search_skills "$SEARCH_KEYWORD"
3977
3094
  elif [ -n "$ADD_SKILL_URL" ]; then