cortexhawk 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/README.md +25 -7
- package/commands/cleanup.md +1 -0
- package/cortexhawk +7 -1
- package/hooks/branch-guard.sh +1 -2
- package/hooks/codex-dispatcher.sh +3 -0
- package/install.sh +55 -934
- package/mcp/context7.json +1 -1
- package/mcp/github.json +1 -1
- package/mcp/puppeteer.json +1 -1
- package/mcp/sequential-thinking.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.sh +164 -0
- package/scripts/install-claude.sh +179 -0
- package/scripts/post-merge-cleanup.sh +170 -80
- package/scripts/restore.sh +212 -0
- package/scripts/snapshot.sh +163 -0
- package/scripts/update.sh +280 -0
- package/templates/AGENT.md +19 -0
- package/templates/CLAUDE.md.template +41 -0
- package/templates/COMMAND.md +14 -0
- package/templates/ORCHESTRATION.md +79 -0
- package/templates/PERSONA.md +17 -0
- package/templates/SKILL.md +17 -0
- package/templates/github/PULL_REQUEST_TEMPLATE.md +26 -0
- package/templates/github/gitmessage +10 -0
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
|
|
@@ -735,9 +740,11 @@ setup_templates() {
|
|
|
735
740
|
if [ -n "$pr_template" ]; then
|
|
736
741
|
green " PR template found: $pr_template"
|
|
737
742
|
else
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
743
|
+
if [ -f "$SCRIPT_DIR/templates/github/PULL_REQUEST_TEMPLATE.md" ]; then
|
|
744
|
+
mkdir -p "$project_root/.github"
|
|
745
|
+
cp "$SCRIPT_DIR/templates/github/PULL_REQUEST_TEMPLATE.md" "$project_root/.github/PULL_REQUEST_TEMPLATE.md"
|
|
746
|
+
green " PR template created: .github/PULL_REQUEST_TEMPLATE.md"
|
|
747
|
+
fi
|
|
741
748
|
fi
|
|
742
749
|
|
|
743
750
|
# Commit template
|
|
@@ -752,8 +759,10 @@ setup_templates() {
|
|
|
752
759
|
if [ -n "$commit_template" ]; then
|
|
753
760
|
green " Commit template found: $commit_template"
|
|
754
761
|
else
|
|
755
|
-
|
|
756
|
-
|
|
762
|
+
if [ -f "$SCRIPT_DIR/templates/github/gitmessage" ]; then
|
|
763
|
+
cp "$SCRIPT_DIR/templates/github/gitmessage" "$project_root/.gitmessage"
|
|
764
|
+
green " Commit template created: .gitmessage"
|
|
765
|
+
fi
|
|
757
766
|
fi
|
|
758
767
|
}
|
|
759
768
|
|
|
@@ -916,6 +925,10 @@ cleanup_update() {
|
|
|
916
925
|
}
|
|
917
926
|
|
|
918
927
|
update_source_git() {
|
|
928
|
+
if ! git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
|
929
|
+
echo " CortexHawk installed via npm — run: npm update -g cortexhawk"
|
|
930
|
+
exit 0
|
|
931
|
+
fi
|
|
919
932
|
local current_branch
|
|
920
933
|
current_branch=$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
|
921
934
|
if [ "$current_branch" != "main" ]; then
|
|
@@ -1186,431 +1199,6 @@ count_component_files() {
|
|
|
1186
1199
|
done
|
|
1187
1200
|
}
|
|
1188
1201
|
|
|
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
1202
|
# --- do_snapshots_list() ---
|
|
1615
1203
|
do_snapshots_list() {
|
|
1616
1204
|
if [ "$GLOBAL" = true ]; then
|
|
@@ -1960,358 +1548,6 @@ do_export_team() {
|
|
|
1960
1548
|
echo "Share this file with your team. Install with: install.sh --team"
|
|
1961
1549
|
}
|
|
1962
1550
|
|
|
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
1551
|
# --- do_uninstall() ---
|
|
2316
1552
|
# Remove CortexHawk installation cleanly
|
|
2317
1553
|
do_uninstall() {
|
|
@@ -2990,161 +2226,37 @@ do_team_install() {
|
|
|
2990
2226
|
echo "Team install complete."
|
|
2991
2227
|
}
|
|
2992
2228
|
|
|
2993
|
-
# ---
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
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
|
|
2229
|
+
# --- install_git_post_merge_hook(project_root) ---
|
|
2230
|
+
# Installs .git/hooks/post-merge to auto-run cleanup after git merge (opt-in)
|
|
2231
|
+
install_git_post_merge_hook() {
|
|
2232
|
+
local project_root="${1:-$(pwd)}"
|
|
2233
|
+
local git_dir="$project_root/.git"
|
|
2234
|
+
if [ ! -d "$git_dir" ]; then
|
|
2235
|
+
yellow " Warning: no .git directory found — skipping post-merge hook"
|
|
2236
|
+
return 0
|
|
3014
2237
|
fi
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
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"
|
|
2238
|
+
mkdir -p "$git_dir/hooks"
|
|
2239
|
+
local hook_path="$git_dir/hooks/post-merge"
|
|
2240
|
+
if [ -f "$hook_path" ]; then
|
|
2241
|
+
if grep -q "CortexHawk" "$hook_path" 2>/dev/null; then
|
|
2242
|
+
echo " post-merge hook: already installed"
|
|
3045
2243
|
else
|
|
3046
|
-
|
|
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"
|
|
2244
|
+
yellow " post-merge hook: skipped (custom hook exists at .git/hooks/post-merge)"
|
|
3123
2245
|
fi
|
|
2246
|
+
return 0
|
|
3124
2247
|
fi
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
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."
|
|
2248
|
+
# Write hook — calls cleanup script in auto mode, never blocks the merge
|
|
2249
|
+
cat > "$hook_path" <<'GITHOOK'
|
|
2250
|
+
#!/bin/bash
|
|
2251
|
+
# post-merge — CortexHawk auto-cleanup after git merge
|
|
2252
|
+
# Installed by CortexHawk. Remove this file to disable.
|
|
2253
|
+
if [ -f ".claude/scripts/post-merge-cleanup.sh" ]; then
|
|
2254
|
+
bash ".claude/scripts/post-merge-cleanup.sh" --auto 2>/dev/null || true
|
|
2255
|
+
fi
|
|
2256
|
+
exit 0
|
|
2257
|
+
GITHOOK
|
|
2258
|
+
chmod +x "$hook_path"
|
|
2259
|
+
green " → native git post-merge hook installed (.git/hooks/post-merge)"
|
|
3148
2260
|
}
|
|
3149
2261
|
|
|
3150
2262
|
# --- install_kimi() ---
|
|
@@ -3931,6 +3043,13 @@ UTILSJS
|
|
|
3931
3043
|
echo ""
|
|
3932
3044
|
}
|
|
3933
3045
|
|
|
3046
|
+
# --- Source extracted modules ---
|
|
3047
|
+
source "$SCRIPT_DIR/scripts/install-claude.sh"
|
|
3048
|
+
source "$SCRIPT_DIR/scripts/update.sh"
|
|
3049
|
+
source "$SCRIPT_DIR/scripts/snapshot.sh"
|
|
3050
|
+
source "$SCRIPT_DIR/scripts/restore.sh"
|
|
3051
|
+
source "$SCRIPT_DIR/scripts/doctor.sh"
|
|
3052
|
+
|
|
3934
3053
|
# --- Dispatcher ---
|
|
3935
3054
|
if [ "$DEMO_MODE" = true ]; then
|
|
3936
3055
|
do_demo
|
|
@@ -3972,6 +3091,8 @@ elif [ -n "$ENABLE_HOOK" ]; then
|
|
|
3972
3091
|
do_toggle_hook "$ENABLE_HOOK" "enable"
|
|
3973
3092
|
elif [ -n "$DISABLE_HOOK" ]; then
|
|
3974
3093
|
do_toggle_hook "$DISABLE_HOOK" "disable"
|
|
3094
|
+
elif [ "$POST_MERGE_HOOK_MODE" = true ]; then
|
|
3095
|
+
install_git_post_merge_hook "$(pwd)"
|
|
3975
3096
|
elif [ -n "$SEARCH_KEYWORD" ]; then
|
|
3976
3097
|
do_search_skills "$SEARCH_KEYWORD"
|
|
3977
3098
|
elif [ -n "$ADD_SKILL_URL" ]; then
|