cortexhawk 3.1.1 → 3.3.0
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 +46 -1
- package/CLAUDE.md +12 -4
- package/README.md +20 -5
- package/agents/git-manager.md +10 -2
- package/commands/backlog.md +1 -0
- package/commands/cleanup.md +36 -0
- package/commands/commit.md +24 -0
- package/commands/review-pr.md +31 -0
- package/commands/ship.md +2 -1
- package/commands/task.md +1 -0
- package/cortexhawk +2 -2
- package/hooks/branch-guard.sh +9 -1
- 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 +8 -5
- package/install.sh +370 -124
- package/mcp/README.md +36 -0
- package/mcp/github.json +11 -0
- package/package.json +1 -1
- package/profiles/api.json +2 -1
- package/profiles/fullstack.json +2 -1
- package/scripts/autodetect-profile.sh +6 -2
- package/scripts/interactive-init.sh +20 -12
- package/scripts/lint-guard-runner.sh +132 -0
- package/scripts/post-merge-cleanup.sh +143 -0
- package/scripts/refresh-context.sh +51 -0
- package/settings.json +12 -1
package/install.sh
CHANGED
|
@@ -7,7 +7,24 @@ 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
|
|
20
|
+
# Strip double quotes
|
|
10
21
|
value="${value%\"}" && value="${value#\"}"
|
|
22
|
+
# Strip single quotes
|
|
23
|
+
value="${value%'}" && value="${value#'}"
|
|
24
|
+
# Strip inline comments (space + # marks start of comment)
|
|
25
|
+
value="$(printf '%s\n' "$value" | sed 's/[[:space:]]#.*$//')"
|
|
26
|
+
# Strip trailing whitespace
|
|
27
|
+
value="${value%"${value##*[! ]}"}"
|
|
11
28
|
export "$key=$value" 2>/dev/null || true
|
|
12
29
|
done < .env
|
|
13
30
|
fi
|
|
@@ -20,6 +37,15 @@ get_version() {
|
|
|
20
37
|
grep -m1 '## \[' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
|
|
21
38
|
}
|
|
22
39
|
|
|
40
|
+
# Portable sed -i (GNU vs BSD)
|
|
41
|
+
sed_inplace() {
|
|
42
|
+
if sed --version 2>/dev/null | grep -q GNU; then
|
|
43
|
+
sed -i "$@"
|
|
44
|
+
else
|
|
45
|
+
sed -i '' "$@"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
compute_checksum() {
|
|
24
50
|
local file="$1"
|
|
25
51
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
@@ -106,6 +132,7 @@ print_usage() {
|
|
|
106
132
|
echo " --dry-run Simulate install/update without writing files"
|
|
107
133
|
echo " --test-hooks Dry-run all hooks with synthetic inputs"
|
|
108
134
|
echo " --no-scan Skip post-install security audit"
|
|
135
|
+
echo " --version, -v Show CortexHawk version"
|
|
109
136
|
echo " --help, -h Show this help"
|
|
110
137
|
}
|
|
111
138
|
|
|
@@ -144,15 +171,41 @@ STATS_MODE=false
|
|
|
144
171
|
PUBLISH_SKILL_PATH=""
|
|
145
172
|
CHECK_UPDATE_MODE=false
|
|
146
173
|
DEMO_MODE=false
|
|
174
|
+
TRUST_SKILL=false
|
|
147
175
|
MAX_SNAPSHOTS=10
|
|
148
176
|
|
|
177
|
+
# === Component Registry ===
|
|
178
|
+
# Single source of truth for all CortexHawk components.
|
|
179
|
+
# Format: "name:executable"
|
|
180
|
+
# - executable = "yes" runs chmod +x *.sh after copy (hooks, scripts)
|
|
181
|
+
# - executable = "no" for markdown-only components (agents, commands, modes)
|
|
182
|
+
# - skills handled specially via copy_skills()/sync_skills_update() for profile filtering
|
|
183
|
+
# Used by: copy_all_components(), sync_all_components(), count_component_files()
|
|
184
|
+
COMPONENTS=(
|
|
185
|
+
"agents:no"
|
|
186
|
+
"commands:no"
|
|
187
|
+
"skills:no"
|
|
188
|
+
"hooks:yes"
|
|
189
|
+
"modes:no"
|
|
190
|
+
"mcp:no"
|
|
191
|
+
"scripts:yes"
|
|
192
|
+
)
|
|
193
|
+
|
|
149
194
|
while [ $# -gt 0 ]; do
|
|
150
195
|
case "$1" in
|
|
151
196
|
--target)
|
|
197
|
+
if [ -z "${2:-}" ]; then
|
|
198
|
+
echo "Error: --target requires a value (claude|kimi|codex|auto|all)"
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
152
201
|
TARGET_CLI="$2"
|
|
153
202
|
shift 2
|
|
154
203
|
;;
|
|
155
204
|
--profile)
|
|
205
|
+
if [ -z "${2:-}" ]; then
|
|
206
|
+
echo "Error: --profile requires a value (fullstack|api|data)"
|
|
207
|
+
exit 1
|
|
208
|
+
fi
|
|
156
209
|
PROFILE="$2"
|
|
157
210
|
shift 2
|
|
158
211
|
;;
|
|
@@ -218,6 +271,10 @@ while [ $# -gt 0 ]; do
|
|
|
218
271
|
;;
|
|
219
272
|
--restore)
|
|
220
273
|
RESTORE_MODE=true
|
|
274
|
+
if [ -z "${2:-}" ]; then
|
|
275
|
+
echo "Error: --restore requires a snapshot path or --latest"
|
|
276
|
+
exit 1
|
|
277
|
+
fi
|
|
221
278
|
SNAPSHOT_FILE="$2"
|
|
222
279
|
if [ "$SNAPSHOT_FILE" = "--latest" ]; then
|
|
223
280
|
if [ "$GLOBAL" = true ]; then
|
|
@@ -271,6 +328,10 @@ while [ $# -gt 0 ]; do
|
|
|
271
328
|
DEMO_MODE=true
|
|
272
329
|
shift
|
|
273
330
|
;;
|
|
331
|
+
--trust)
|
|
332
|
+
TRUST_SKILL=true
|
|
333
|
+
shift
|
|
334
|
+
;;
|
|
274
335
|
--publish-skill)
|
|
275
336
|
PUBLISH_SKILL_PATH="$2"
|
|
276
337
|
if [ -z "$PUBLISH_SKILL_PATH" ]; then
|
|
@@ -323,6 +384,10 @@ while [ $# -gt 0 ]; do
|
|
|
323
384
|
fi
|
|
324
385
|
shift 2
|
|
325
386
|
;;
|
|
387
|
+
--version|-v)
|
|
388
|
+
echo "CortexHawk $(get_version)"
|
|
389
|
+
exit 0
|
|
390
|
+
;;
|
|
326
391
|
--help|-h)
|
|
327
392
|
print_usage
|
|
328
393
|
exit 0
|
|
@@ -368,14 +433,6 @@ if [ "$RESTORE_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" =
|
|
|
368
433
|
echo "Error: --restore cannot be combined with --init or --update"
|
|
369
434
|
exit 1
|
|
370
435
|
fi
|
|
371
|
-
if [ "$TARGET_CLI" = "all" ] && [ "$INIT_MODE" = true ]; then
|
|
372
|
-
echo "Error: --target all cannot be combined with --init (run --init per target instead)"
|
|
373
|
-
exit 1
|
|
374
|
-
fi
|
|
375
|
-
if [ "$TARGET_CLI" = "auto" ] && [ "$INIT_MODE" = true ]; then
|
|
376
|
-
echo "Error: --target auto cannot be combined with --init (run --init per target instead)"
|
|
377
|
-
exit 1
|
|
378
|
-
fi
|
|
379
436
|
if [ -n "$ADD_SKILL_URL" ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
|
|
380
437
|
echo "Error: --add-skill cannot be combined with --init, --update, --snapshot, or --restore"
|
|
381
438
|
exit 1
|
|
@@ -430,7 +487,7 @@ if [ -n "$PACK_NAME" ]; then
|
|
|
430
487
|
exit 1
|
|
431
488
|
fi
|
|
432
489
|
_skills_csv=$(echo "$_row" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
|
|
433
|
-
_description=$(echo "$_row" | awk -F'|' '{print $4}' | sed 's/^ *//;s/ *$//')
|
|
490
|
+
_description=$(echo "$_row" | awk -F'|' '{print $4}' | sed 's/^ *//;s/ *$//' | sed 's/"/\\"/g')
|
|
434
491
|
_tmp_profile=$(mktemp /tmp/cortexhawk-pack-XXXXXX.json)
|
|
435
492
|
{
|
|
436
493
|
echo '{'
|
|
@@ -480,12 +537,17 @@ detect_installed_clis() {
|
|
|
480
537
|
copy_skills() {
|
|
481
538
|
local target_dir="$1"
|
|
482
539
|
local profile="$2"
|
|
540
|
+
local source_dir="${3:-$SCRIPT_DIR}"
|
|
483
541
|
if [ -z "$profile" ]; then
|
|
484
|
-
cp -r "$
|
|
542
|
+
cp -r "$source_dir/skills/"* "$target_dir/skills/" 2>/dev/null || true
|
|
485
543
|
else
|
|
486
544
|
grep '"[a-z]' "$PROFILE_FILE" | sed 's/.*"\([a-z][^"]*\)".*/\1/' | grep '/' | while read -r skill; do
|
|
487
|
-
local src="$
|
|
545
|
+
local src="$source_dir/skills/$skill"
|
|
488
546
|
local dest="$target_dir/skills/$skill"
|
|
547
|
+
if [ ! -d "$src" ]; then
|
|
548
|
+
yellow " Warning: skill '$skill' not found in source — skipping"
|
|
549
|
+
continue
|
|
550
|
+
fi
|
|
489
551
|
mkdir -p "$(dirname "$dest")"
|
|
490
552
|
cp -r "$src" "$dest" 2>/dev/null || true
|
|
491
553
|
done
|
|
@@ -545,6 +607,9 @@ convert_modes_to_skills() {
|
|
|
545
607
|
done
|
|
546
608
|
}
|
|
547
609
|
|
|
610
|
+
# NOTE: profiles/*.json contain an "mcp" field listing recommended servers, but install.sh
|
|
611
|
+
# currently installs all mcp/*.json regardless of profile. Per-profile MCP filtering is planned
|
|
612
|
+
# for a future phase of #137 (install.sh modularization).
|
|
548
613
|
# merge_mcp_json(output_file) — merges mcp/*.json into single JSON file
|
|
549
614
|
merge_mcp_json() {
|
|
550
615
|
local output_file="$1"
|
|
@@ -553,7 +618,7 @@ merge_mcp_json() {
|
|
|
553
618
|
[ -f "$mcp_file" ] || continue
|
|
554
619
|
local server_name command_val args_val entry
|
|
555
620
|
server_name=$(basename "$mcp_file" .json)
|
|
556
|
-
command_val=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
|
621
|
+
command_val=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | sed 's/"/\\"/g')
|
|
557
622
|
args_val=$(grep '"args"' "$mcp_file" | sed 's/.*"args"[[:space:]]*:[[:space:]]*\(\[.*\]\).*/\1/')
|
|
558
623
|
entry=" \"$server_name\": {\n \"command\": \"$command_val\",\n \"args\": $args_val\n }"
|
|
559
624
|
if [ -n "$entries" ]; then
|
|
@@ -594,11 +659,34 @@ update_gitignore() {
|
|
|
594
659
|
touch "$gitignore"
|
|
595
660
|
fi
|
|
596
661
|
|
|
597
|
-
#
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
662
|
+
# Ensure essential entries are present (disable glob to keep *.log literal)
|
|
663
|
+
local essentials="node_modules/ __pycache__/ .env .env.local *.log dist/ build/ .DS_Store"
|
|
664
|
+
local added_count=0
|
|
665
|
+
set -f
|
|
666
|
+
for entry in $essentials; do
|
|
667
|
+
if ! grep -qxF "$entry" "$gitignore" 2>/dev/null; then
|
|
668
|
+
echo "$entry" >> "$gitignore"
|
|
669
|
+
added_count=$((added_count + 1))
|
|
670
|
+
fi
|
|
671
|
+
done
|
|
672
|
+
set +f
|
|
673
|
+
if [ "$added_count" -gt 0 ]; then
|
|
674
|
+
green " Added $added_count essential entries to .gitignore (.env, node_modules/, etc.)"
|
|
675
|
+
fi
|
|
676
|
+
|
|
677
|
+
# Auto-add target directory (always), group under single header
|
|
678
|
+
if ! grep -qxF "$target_dir/" "$gitignore" 2>/dev/null; then
|
|
679
|
+
if grep -q "# CortexHawk" "$gitignore" 2>/dev/null; then
|
|
680
|
+
# Append under the first existing header only
|
|
681
|
+
local header_line
|
|
682
|
+
header_line=$(grep -n "# CortexHawk" "$gitignore" | head -1 | cut -d: -f1)
|
|
683
|
+
sed_inplace "${header_line}a\\
|
|
684
|
+
$target_dir/" "$gitignore"
|
|
685
|
+
else
|
|
686
|
+
echo "" >> "$gitignore"
|
|
687
|
+
echo "# CortexHawk" >> "$gitignore"
|
|
688
|
+
echo "$target_dir/" >> "$gitignore"
|
|
689
|
+
fi
|
|
602
690
|
green " Added $target_dir/ to .gitignore"
|
|
603
691
|
fi
|
|
604
692
|
|
|
@@ -606,19 +694,69 @@ update_gitignore() {
|
|
|
606
694
|
if [ -d "$project_root/docs" ] && ! grep -qx "docs/" "$gitignore" 2>/dev/null; then
|
|
607
695
|
echo ""
|
|
608
696
|
echo " docs/ contains agent outputs (brainstorms, plans, research)."
|
|
609
|
-
|
|
697
|
+
if [ -t 0 ]; then
|
|
698
|
+
read -r -p " Track docs/ in git? [Y/n]: " docs_choice
|
|
699
|
+
else
|
|
700
|
+
docs_choice="Y"
|
|
701
|
+
fi
|
|
610
702
|
case "$docs_choice" in
|
|
611
703
|
[nN]*)
|
|
612
704
|
echo "docs/" >> "$gitignore"
|
|
613
705
|
green " Added docs/ to .gitignore"
|
|
614
706
|
;;
|
|
615
707
|
*)
|
|
616
|
-
green " docs/ will be tracked in git"
|
|
708
|
+
green " docs/ will be tracked in git (default)"
|
|
617
709
|
;;
|
|
618
710
|
esac
|
|
619
711
|
fi
|
|
620
712
|
}
|
|
621
713
|
|
|
714
|
+
# --- setup_templates() ---
|
|
715
|
+
# Detects existing PR/commit templates; generates CortexHawk defaults if missing
|
|
716
|
+
# Args: $1=project_root
|
|
717
|
+
setup_templates() {
|
|
718
|
+
local project_root="$1"
|
|
719
|
+
|
|
720
|
+
[ "$GLOBAL" = true ] && return
|
|
721
|
+
|
|
722
|
+
# PR template
|
|
723
|
+
local pr_template=""
|
|
724
|
+
for path in ".github/PULL_REQUEST_TEMPLATE.md" ".github/pull_request_template.md" "docs/pull_request_template.md"; do
|
|
725
|
+
if [ -f "$project_root/$path" ]; then
|
|
726
|
+
pr_template="$path"
|
|
727
|
+
break
|
|
728
|
+
fi
|
|
729
|
+
done
|
|
730
|
+
# Also check template directory
|
|
731
|
+
if [ -z "$pr_template" ] && [ -d "$project_root/.github/PULL_REQUEST_TEMPLATE" ]; then
|
|
732
|
+
pr_template=".github/PULL_REQUEST_TEMPLATE/"
|
|
733
|
+
fi
|
|
734
|
+
|
|
735
|
+
if [ -n "$pr_template" ]; then
|
|
736
|
+
green " PR template found: $pr_template"
|
|
737
|
+
else
|
|
738
|
+
mkdir -p "$project_root/.github"
|
|
739
|
+
cp "$SCRIPT_DIR/templates/github/PULL_REQUEST_TEMPLATE.md" "$project_root/.github/PULL_REQUEST_TEMPLATE.md"
|
|
740
|
+
green " PR template created: .github/PULL_REQUEST_TEMPLATE.md"
|
|
741
|
+
fi
|
|
742
|
+
|
|
743
|
+
# Commit template
|
|
744
|
+
local commit_template=""
|
|
745
|
+
for path in ".gitmessage" ".github/gitmessage" ".github/commit-template"; do
|
|
746
|
+
if [ -f "$project_root/$path" ]; then
|
|
747
|
+
commit_template="$path"
|
|
748
|
+
break
|
|
749
|
+
fi
|
|
750
|
+
done
|
|
751
|
+
|
|
752
|
+
if [ -n "$commit_template" ]; then
|
|
753
|
+
green " Commit template found: $commit_template"
|
|
754
|
+
else
|
|
755
|
+
cp "$SCRIPT_DIR/templates/github/gitmessage" "$project_root/.gitmessage"
|
|
756
|
+
green " Commit template created: .gitmessage"
|
|
757
|
+
fi
|
|
758
|
+
}
|
|
759
|
+
|
|
622
760
|
# --- run_audit() ---
|
|
623
761
|
run_audit() {
|
|
624
762
|
local project_root="$1"
|
|
@@ -641,11 +779,12 @@ generate_hooks_config() {
|
|
|
641
779
|
fi
|
|
642
780
|
|
|
643
781
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
782
|
+
yellow " Warning: python3 not found — hooks config will use static fallback" >&2
|
|
644
783
|
return 1
|
|
645
784
|
fi
|
|
646
785
|
|
|
647
|
-
python3 << PYEOF
|
|
648
|
-
import re, json, sys
|
|
786
|
+
python3 - "$compose_file" "$hooks_dir" << 'PYEOF'
|
|
787
|
+
import re, json, sys, shlex
|
|
649
788
|
|
|
650
789
|
def parse_compose(path, hooks_dir):
|
|
651
790
|
"""Parse compose.yml and generate Claude Code hooks JSON."""
|
|
@@ -679,10 +818,13 @@ def parse_compose(path, hooks_dir):
|
|
|
679
818
|
# Hook item
|
|
680
819
|
elif line.strip().startswith('- '):
|
|
681
820
|
hook_name = line.strip()[2:].strip()
|
|
821
|
+
if not re.match(r'^[a-zA-Z0-9_-]+$', hook_name):
|
|
822
|
+
print(f"Warning: skipping invalid hook name: {hook_name}", file=sys.stderr)
|
|
823
|
+
continue
|
|
682
824
|
hook_path = f"{hooks_dir}/{hook_name}.sh"
|
|
683
825
|
|
|
684
826
|
# Build command — hooks read stdin JSON (Claude Code protocol)
|
|
685
|
-
cmd = f'bash {hook_path}'
|
|
827
|
+
cmd = f'bash {shlex.quote(hook_path)}'
|
|
686
828
|
|
|
687
829
|
hook_entry = {"type": "command", "command": cmd}
|
|
688
830
|
|
|
@@ -708,7 +850,7 @@ def parse_compose(path, hooks_dir):
|
|
|
708
850
|
return result
|
|
709
851
|
|
|
710
852
|
try:
|
|
711
|
-
result = parse_compose(
|
|
853
|
+
result = parse_compose(sys.argv[1], sys.argv[2])
|
|
712
854
|
print(json.dumps(result, indent=2))
|
|
713
855
|
except Exception as e:
|
|
714
856
|
print(f"Error: {e}", file=sys.stderr)
|
|
@@ -786,7 +928,8 @@ update_source_git() {
|
|
|
786
928
|
}
|
|
787
929
|
|
|
788
930
|
update_source_release() {
|
|
789
|
-
local tmp_dir
|
|
931
|
+
local tmp_dir
|
|
932
|
+
tmp_dir=$(mktemp -d /tmp/cortexhawk-update-XXXXXX)
|
|
790
933
|
local repo_url
|
|
791
934
|
repo_url=$(get_source_url)
|
|
792
935
|
if [ -z "$repo_url" ] && [ -f "$TARGET/.cortexhawk-manifest" ]; then
|
|
@@ -797,13 +940,19 @@ update_source_release() {
|
|
|
797
940
|
echo "Set CORTEXHAWK_REPO environment variable and retry"
|
|
798
941
|
exit 1
|
|
799
942
|
fi
|
|
800
|
-
mkdir -p "$tmp_dir"
|
|
801
943
|
echo " Downloading from $repo_url..."
|
|
802
|
-
|
|
944
|
+
local tarball="$tmp_dir/update.tar.gz"
|
|
945
|
+
if ! curl -sL "$repo_url/archive/refs/heads/main.tar.gz" -o "$tarball"; then
|
|
803
946
|
echo "Error: failed to download latest release"
|
|
804
947
|
rm -rf "$tmp_dir"
|
|
805
948
|
exit 1
|
|
806
949
|
fi
|
|
950
|
+
if ! tar xzf "$tarball" -C "$tmp_dir" --strip-components=1; then
|
|
951
|
+
echo "Error: failed to extract update archive"
|
|
952
|
+
rm -rf "$tmp_dir"
|
|
953
|
+
exit 1
|
|
954
|
+
fi
|
|
955
|
+
rm -f "$tarball"
|
|
807
956
|
SCRIPT_DIR="$tmp_dir"
|
|
808
957
|
UPDATE_CLEANUP_DIR="$tmp_dir"
|
|
809
958
|
}
|
|
@@ -995,6 +1144,48 @@ sync_skills_update() {
|
|
|
995
1144
|
fi
|
|
996
1145
|
}
|
|
997
1146
|
|
|
1147
|
+
# === Generic Component Operations ===
|
|
1148
|
+
# These use the COMPONENTS array to avoid hardcoding directory lists.
|
|
1149
|
+
|
|
1150
|
+
# copy_all_components(source_dir, target_dir, profile)
|
|
1151
|
+
copy_all_components() {
|
|
1152
|
+
local src="$1" dst="$2" profile="$3"
|
|
1153
|
+
for entry in "${COMPONENTS[@]}"; do
|
|
1154
|
+
IFS=':' read -r name exec_flag <<< "$entry"
|
|
1155
|
+
[ -d "$src/$name" ] || continue
|
|
1156
|
+
mkdir -p "$dst/$name"
|
|
1157
|
+
if [ "$name" = "skills" ] && [ -n "$profile" ]; then
|
|
1158
|
+
copy_skills "$dst" "$profile" "$src"
|
|
1159
|
+
else
|
|
1160
|
+
cp -r "$src/$name/"* "$dst/$name/" 2>/dev/null || true
|
|
1161
|
+
fi
|
|
1162
|
+
[ "$exec_flag" = "yes" ] && chmod +x "$dst/$name/"*.sh 2>/dev/null || true
|
|
1163
|
+
done
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
# sync_all_components(profile)
|
|
1167
|
+
sync_all_components() {
|
|
1168
|
+
local profile="$1"
|
|
1169
|
+
for entry in "${COMPONENTS[@]}"; do
|
|
1170
|
+
IFS=':' read -r name exec_flag <<< "$entry"
|
|
1171
|
+
if [ "$name" = "skills" ]; then
|
|
1172
|
+
sync_skills_update "$profile"
|
|
1173
|
+
else
|
|
1174
|
+
sync_component "$name"
|
|
1175
|
+
fi
|
|
1176
|
+
done
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
# count_component_files(source_dir)
|
|
1180
|
+
count_component_files() {
|
|
1181
|
+
local src="$1"
|
|
1182
|
+
for entry in "${COMPONENTS[@]}"; do
|
|
1183
|
+
IFS=':' read -r name _ <<< "$entry"
|
|
1184
|
+
local c; c=$(find "$src/$name" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
1185
|
+
printf " %-12s %s files\n" "$name/" "$c"
|
|
1186
|
+
done
|
|
1187
|
+
}
|
|
1188
|
+
|
|
998
1189
|
do_update() {
|
|
999
1190
|
# 1. Validate target
|
|
1000
1191
|
if [ "$TARGET_CLI" != "claude" ]; then
|
|
@@ -1143,12 +1334,7 @@ do_update() {
|
|
|
1143
1334
|
SYNC_SKIPPED=0
|
|
1144
1335
|
SYNC_CONFLICTS=0
|
|
1145
1336
|
|
|
1146
|
-
|
|
1147
|
-
sync_component "commands"
|
|
1148
|
-
sync_skills_update "$update_profile"
|
|
1149
|
-
sync_component "hooks"
|
|
1150
|
-
sync_component "modes"
|
|
1151
|
-
sync_component "mcp"
|
|
1337
|
+
sync_all_components "$update_profile"
|
|
1152
1338
|
|
|
1153
1339
|
# 6b. Sync agent personas from project root
|
|
1154
1340
|
local project_root
|
|
@@ -1173,33 +1359,46 @@ do_update() {
|
|
|
1173
1359
|
fi
|
|
1174
1360
|
|
|
1175
1361
|
if [ "$DRY_RUN" != true ]; then
|
|
1176
|
-
# Make
|
|
1177
|
-
|
|
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
|
|
1178
1367
|
|
|
1179
|
-
# 7b. Regenerate settings.json hooks
|
|
1368
|
+
# 7b. Regenerate settings.json hooks + merge new permissions from compose.yml
|
|
1180
1369
|
if [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
|
|
1181
1370
|
local hooks_json
|
|
1182
1371
|
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
1183
|
-
if [ -n "$hooks_json" ]; then
|
|
1184
|
-
python3
|
|
1372
|
+
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
1373
|
+
echo "$hooks_json" | python3 -c "
|
|
1185
1374
|
import json, sys
|
|
1186
|
-
|
|
1187
|
-
# Read current settings.json to preserve permissions
|
|
1375
|
+
hooks = json.load(sys.stdin)
|
|
1188
1376
|
current = {}
|
|
1189
1377
|
try:
|
|
1190
|
-
with open(
|
|
1378
|
+
with open(sys.argv[1]) as f:
|
|
1191
1379
|
current = json.load(f)
|
|
1192
1380
|
except:
|
|
1193
1381
|
pass
|
|
1194
|
-
|
|
1195
|
-
# Merge: keep existing permissions, replace hooks
|
|
1196
|
-
hooks = json.loads('''$hooks_json''')
|
|
1197
1382
|
current['hooks'] = hooks
|
|
1198
|
-
|
|
1199
|
-
|
|
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:
|
|
1200
1399
|
json.dump(current, f, indent=2)
|
|
1201
1400
|
f.write('\n')
|
|
1202
|
-
|
|
1401
|
+
" "$TARGET/settings.json" "$SCRIPT_DIR/settings.json"
|
|
1203
1402
|
echo " Regenerated settings.json hooks from compose.yml"
|
|
1204
1403
|
fi
|
|
1205
1404
|
fi
|
|
@@ -1214,6 +1413,7 @@ PYEOF
|
|
|
1214
1413
|
local target_dir_name=".${TARGET_CLI:-claude}"
|
|
1215
1414
|
update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
|
|
1216
1415
|
[ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
|
|
1416
|
+
setup_templates "$(dirname "$TARGET")"
|
|
1217
1417
|
|
|
1218
1418
|
if [ ! -f "$TARGET/git-workflow.conf" ]; then
|
|
1219
1419
|
GIT_BRANCHING="direct-main"
|
|
@@ -1313,12 +1513,10 @@ do_snapshot() {
|
|
|
1313
1513
|
git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
|
|
1314
1514
|
fi
|
|
1315
1515
|
|
|
1316
|
-
# Read custom profile if applicable
|
|
1516
|
+
# Read custom profile if applicable (use PROFILE_FILE from current run, never glob /tmp/)
|
|
1317
1517
|
local profile_def="null"
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if [ -n "$custom_profile" ] && [ -f "$custom_profile" ]; then
|
|
1321
|
-
profile_def=$(cat "$custom_profile")
|
|
1518
|
+
if [ -n "$PROFILE_FILE" ] && [ -f "$PROFILE_FILE" ]; then
|
|
1519
|
+
profile_def=$(cat "$PROFILE_FILE")
|
|
1322
1520
|
fi
|
|
1323
1521
|
|
|
1324
1522
|
# Build files checksums from manifest
|
|
@@ -1369,7 +1567,7 @@ do_snapshot() {
|
|
|
1369
1567
|
[ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
|
|
1370
1568
|
[ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
|
|
1371
1569
|
# Remove trailing comma from last entry
|
|
1372
|
-
|
|
1570
|
+
sed_inplace '$ s/,$//' "$snap_file"
|
|
1373
1571
|
printf ' }\n' >> "$snap_file"
|
|
1374
1572
|
printf '}\n' >> "$snap_file"
|
|
1375
1573
|
|
|
@@ -1850,40 +2048,28 @@ do_restore() {
|
|
|
1850
2048
|
|
|
1851
2049
|
# Reinstall using the standard flow
|
|
1852
2050
|
echo "Reinstalling CortexHawk components..."
|
|
1853
|
-
mkdir -p "$TARGET"/{agents,commands,skills,hooks,modes,mcp}
|
|
1854
2051
|
|
|
1855
2052
|
# Use portable archive files if available, otherwise use source repo
|
|
1856
2053
|
if [ -n "$portable_files" ] && [ -d "$portable_files" ]; then
|
|
1857
2054
|
echo " Using files from portable archive..."
|
|
1858
|
-
|
|
1859
|
-
cp -r "$portable_files/commands/"* "$TARGET/commands/" 2>/dev/null || true
|
|
1860
|
-
cp -r "$portable_files/skills/"* "$TARGET/skills/" 2>/dev/null || true
|
|
1861
|
-
cp -r "$portable_files/hooks/"* "$TARGET/hooks/" 2>/dev/null || true
|
|
1862
|
-
cp -r "$portable_files/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
1863
|
-
cp -r "$portable_files/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
2055
|
+
copy_all_components "$portable_files" "$TARGET" ""
|
|
1864
2056
|
else
|
|
1865
|
-
|
|
1866
|
-
cp -r "$SCRIPT_DIR/commands/"* "$TARGET/commands/" 2>/dev/null || true
|
|
1867
|
-
copy_skills "$TARGET" "$PROFILE"
|
|
1868
|
-
cp -r "$SCRIPT_DIR/hooks/"* "$TARGET/hooks/" 2>/dev/null || true
|
|
1869
|
-
cp -r "$SCRIPT_DIR/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
1870
|
-
cp -r "$SCRIPT_DIR/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
2057
|
+
copy_all_components "$SCRIPT_DIR" "$TARGET" "$PROFILE"
|
|
1871
2058
|
fi
|
|
1872
|
-
chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
|
|
1873
2059
|
|
|
1874
2060
|
# Restore settings.json from snapshot
|
|
1875
2061
|
if command -v python3 >/dev/null 2>&1; then
|
|
1876
2062
|
python3 -c "
|
|
1877
2063
|
import json, sys
|
|
1878
|
-
with open(
|
|
2064
|
+
with open(sys.argv[1]) as f:
|
|
1879
2065
|
snap = json.load(f)
|
|
1880
2066
|
settings = snap.get('settings')
|
|
1881
2067
|
if settings is not None:
|
|
1882
|
-
with open(
|
|
2068
|
+
with open(sys.argv[2], 'w') as f:
|
|
1883
2069
|
json.dump(settings, f, indent=2)
|
|
1884
2070
|
f.write('\n')
|
|
1885
2071
|
print(' Restored settings.json')
|
|
1886
|
-
"
|
|
2072
|
+
" "$snap_file" "$TARGET/settings.json"
|
|
1887
2073
|
else
|
|
1888
2074
|
echo " Warning: python3 not found — settings.json not restored from snapshot"
|
|
1889
2075
|
fi
|
|
@@ -1909,23 +2095,24 @@ if settings is not None:
|
|
|
1909
2095
|
if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
|
|
1910
2096
|
python3 -c "
|
|
1911
2097
|
import json, base64, sys, os
|
|
1912
|
-
|
|
2098
|
+
snap_file, target_dir = sys.argv[1], sys.argv[2]
|
|
2099
|
+
with open(snap_file) as f:
|
|
1913
2100
|
snap = json.load(f)
|
|
1914
2101
|
contents = snap.get('file_contents', {})
|
|
1915
2102
|
for filename, b64data in contents.items():
|
|
1916
2103
|
try:
|
|
1917
2104
|
data = base64.b64decode(b64data).decode('utf-8')
|
|
1918
2105
|
if filename == 'CLAUDE.md':
|
|
1919
|
-
target_path = os.path.dirname(
|
|
2106
|
+
target_path = os.path.dirname(target_dir) + '/CLAUDE.md'
|
|
1920
2107
|
else:
|
|
1921
|
-
target_path = '
|
|
2108
|
+
target_path = target_dir + '/' + filename
|
|
1922
2109
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
1923
2110
|
with open(target_path, 'w') as f:
|
|
1924
2111
|
f.write(data)
|
|
1925
2112
|
print(f' Restored {filename} (from file_contents)')
|
|
1926
2113
|
except Exception as e:
|
|
1927
2114
|
print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
|
|
1928
|
-
"
|
|
2115
|
+
" "$snap_file" "$TARGET"
|
|
1929
2116
|
fi
|
|
1930
2117
|
|
|
1931
2118
|
# Write new manifest
|
|
@@ -2312,14 +2499,14 @@ do_list_hooks() {
|
|
|
2312
2499
|
printf " %-20s %-14s %-8s %s\n" "Name" "Event" "Status" "Description"
|
|
2313
2500
|
printf " %-20s %-14s %-8s %s\n" "----" "-----" "------" "-----------"
|
|
2314
2501
|
# Parse hooks.json with python3 for reliable JSON handling
|
|
2315
|
-
python3 << PYEOF
|
|
2316
|
-
import json
|
|
2317
|
-
with open(
|
|
2502
|
+
python3 - "$hooks_json" "$compose_file" << 'PYEOF'
|
|
2503
|
+
import json, sys
|
|
2504
|
+
with open(sys.argv[1]) as f:
|
|
2318
2505
|
data = json.load(f)
|
|
2319
2506
|
# Read compose.yml to detect disabled hooks (commented out)
|
|
2320
2507
|
disabled = set()
|
|
2321
2508
|
try:
|
|
2322
|
-
with open(
|
|
2509
|
+
with open(sys.argv[2]) as f:
|
|
2323
2510
|
for line in f:
|
|
2324
2511
|
stripped = line.strip()
|
|
2325
2512
|
if stripped.startswith("# - "):
|
|
@@ -2368,14 +2555,14 @@ do_toggle_hook() {
|
|
|
2368
2555
|
echo "Hook '$hook_name' is already disabled"
|
|
2369
2556
|
return 0
|
|
2370
2557
|
fi
|
|
2371
|
-
|
|
2558
|
+
sed_inplace "s/^ - ${hook_name}$/ # - ${hook_name}/" "$compose_file"
|
|
2372
2559
|
echo "Disabled hook: $hook_name"
|
|
2373
2560
|
else
|
|
2374
2561
|
if grep -q "^ - ${hook_name}$" "$compose_file"; then
|
|
2375
2562
|
echo "Hook '$hook_name' is already enabled"
|
|
2376
2563
|
return 0
|
|
2377
2564
|
fi
|
|
2378
|
-
|
|
2565
|
+
sed_inplace "s/^ # - ${hook_name}$/ - ${hook_name}/" "$compose_file"
|
|
2379
2566
|
echo "Enabled hook: $hook_name"
|
|
2380
2567
|
fi
|
|
2381
2568
|
# Regenerate settings.json if target exists
|
|
@@ -2383,17 +2570,18 @@ do_toggle_hook() {
|
|
|
2383
2570
|
if [ -d "$target" ] && [ -f "$target/settings.json" ]; then
|
|
2384
2571
|
local hooks_json
|
|
2385
2572
|
hooks_json=$(generate_hooks_config "$compose_file" ".claude/hooks")
|
|
2386
|
-
if [ -n "$hooks_json" ]; then
|
|
2387
|
-
python3
|
|
2388
|
-
import json
|
|
2389
|
-
|
|
2573
|
+
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
2574
|
+
echo "$hooks_json" | python3 -c "
|
|
2575
|
+
import json, sys
|
|
2576
|
+
hooks = json.load(sys.stdin)
|
|
2577
|
+
with open(sys.argv[1]) as f:
|
|
2390
2578
|
settings = json.load(f)
|
|
2391
|
-
settings['hooks'] =
|
|
2392
|
-
with open(
|
|
2579
|
+
settings['hooks'] = hooks
|
|
2580
|
+
with open(sys.argv[1], 'w') as f:
|
|
2393
2581
|
json.dump(settings, f, indent=2)
|
|
2394
2582
|
f.write('\n')
|
|
2395
2583
|
print(' Regenerated settings.json')
|
|
2396
|
-
|
|
2584
|
+
" "$target/settings.json"
|
|
2397
2585
|
fi
|
|
2398
2586
|
fi
|
|
2399
2587
|
}
|
|
@@ -2441,17 +2629,18 @@ _search_skillsmp() {
|
|
|
2441
2629
|
fi
|
|
2442
2630
|
|
|
2443
2631
|
# Parse JSON response with python3
|
|
2444
|
-
python3 << PYEOF
|
|
2632
|
+
python3 - "$tmp_response" "$keyword" << 'PYEOF'
|
|
2445
2633
|
import json, sys
|
|
2446
2634
|
try:
|
|
2447
|
-
|
|
2635
|
+
response_file, keyword = sys.argv[1], sys.argv[2]
|
|
2636
|
+
with open(response_file) as f:
|
|
2448
2637
|
data = json.load(f)
|
|
2449
2638
|
container = data.get("data", {})
|
|
2450
2639
|
skills = container.get("skills", [])
|
|
2451
2640
|
pagination = container.get("pagination", {})
|
|
2452
2641
|
total = pagination.get("total", len(skills))
|
|
2453
2642
|
if not skills:
|
|
2454
|
-
print(" No skills found matching '
|
|
2643
|
+
print(f" No skills found matching '{keyword}'")
|
|
2455
2644
|
print("")
|
|
2456
2645
|
print(" Try broader terms or check https://skillsmp.com")
|
|
2457
2646
|
sys.exit(0)
|
|
@@ -2474,7 +2663,7 @@ try:
|
|
|
2474
2663
|
if len(parts) >= 2:
|
|
2475
2664
|
print(f" Install: ./install.sh --add-skill {parts[0]}/{parts[1]}")
|
|
2476
2665
|
break
|
|
2477
|
-
print(f" Browse all: https://skillsmp.com/?q
|
|
2666
|
+
print(f" Browse all: https://skillsmp.com/?q={keyword}")
|
|
2478
2667
|
except Exception as e:
|
|
2479
2668
|
print(f" Error parsing response: {e}")
|
|
2480
2669
|
print(" Falling back to local registry")
|
|
@@ -2593,10 +2782,17 @@ do_add_skill() {
|
|
|
2593
2782
|
exit 1
|
|
2594
2783
|
fi
|
|
2595
2784
|
|
|
2596
|
-
# Security
|
|
2785
|
+
# Security gate: block skills with executable scripts unless --trust is used
|
|
2597
2786
|
if find "$tmp_dir/repo" -name "*.sh" -o -name "*.py" 2>/dev/null | grep -q .; then
|
|
2598
|
-
|
|
2599
|
-
|
|
2787
|
+
if [ "${TRUST_SKILL:-false}" = true ]; then
|
|
2788
|
+
yellow " Warning: this skill contains executable scripts — installing with --trust"
|
|
2789
|
+
else
|
|
2790
|
+
echo " ERROR: This skill contains executable scripts (.sh/.py)."
|
|
2791
|
+
echo " Re-run with --trust to install: install.sh --add-skill $ADD_SKILL_URL --trust"
|
|
2792
|
+
echo " Review the source first: $ADD_SKILL_URL"
|
|
2793
|
+
rm -rf "$tmp_dir"
|
|
2794
|
+
exit 1
|
|
2795
|
+
fi
|
|
2600
2796
|
fi
|
|
2601
2797
|
|
|
2602
2798
|
# Create community directory if needed
|
|
@@ -2717,7 +2913,8 @@ do_team_install() {
|
|
|
2717
2913
|
|
|
2718
2914
|
# Generate temporary profile JSON from skills list
|
|
2719
2915
|
if [ -n "$TEAM_SKILLS" ]; then
|
|
2720
|
-
local tmp_profile
|
|
2916
|
+
local tmp_profile
|
|
2917
|
+
tmp_profile=$(mktemp /tmp/cortexhawk-team-XXXXXX.json)
|
|
2721
2918
|
printf '{\n "name": "team",\n "skills": [\n' > "$tmp_profile"
|
|
2722
2919
|
local first=true
|
|
2723
2920
|
while IFS= read -r skill; do
|
|
@@ -2808,12 +3005,7 @@ install_claude() {
|
|
|
2808
3005
|
echo " Profile: ${PROFILE:-all}"
|
|
2809
3006
|
echo ""
|
|
2810
3007
|
echo "Would install:"
|
|
2811
|
-
|
|
2812
|
-
local c; c=$(find "$SCRIPT_DIR/$comp" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2813
|
-
printf " %-12s %s files\n" "$comp/" "$c"
|
|
2814
|
-
done
|
|
2815
|
-
local sc; sc=$(find "$SCRIPT_DIR/skills" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2816
|
-
printf " %-12s %s files\n" "skills/" "$sc"
|
|
3008
|
+
count_component_files "$SCRIPT_DIR"
|
|
2817
3009
|
echo " settings.json"
|
|
2818
3010
|
[ ! -f "$(pwd)/CLAUDE.md" ] && echo " CLAUDE.md"
|
|
2819
3011
|
echo ""
|
|
@@ -2823,9 +3015,8 @@ install_claude() {
|
|
|
2823
3015
|
|
|
2824
3016
|
echo "Installing for Claude Code to project: $TARGET"
|
|
2825
3017
|
|
|
2826
|
-
|
|
3018
|
+
copy_all_components "$SCRIPT_DIR" "$TARGET" "$PROFILE"
|
|
2827
3019
|
|
|
2828
|
-
cp -r "$SCRIPT_DIR/agents/"* "$TARGET/agents/" 2>/dev/null || true
|
|
2829
3020
|
# Copy agent personas from project root if present
|
|
2830
3021
|
local project_root
|
|
2831
3022
|
project_root="$(dirname "$TARGET")"
|
|
@@ -2835,32 +3026,79 @@ install_claude() {
|
|
|
2835
3026
|
persona_count=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2836
3027
|
[ "$persona_count" -gt 0 ] && echo " Loaded $persona_count agent persona(s) from .cortexhawk-agents/"
|
|
2837
3028
|
fi
|
|
2838
|
-
cp -r "$SCRIPT_DIR/commands/"* "$TARGET/commands/" 2>/dev/null || true
|
|
2839
|
-
copy_skills "$TARGET" "$PROFILE"
|
|
2840
|
-
cp -r "$SCRIPT_DIR/hooks/"* "$TARGET/hooks/" 2>/dev/null || true
|
|
2841
|
-
cp -r "$SCRIPT_DIR/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
2842
|
-
cp -r "$SCRIPT_DIR/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
2843
3029
|
|
|
3030
|
+
local hooks_json
|
|
3031
|
+
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
2844
3032
|
if [ ! -f "$TARGET/settings.json" ]; then
|
|
2845
|
-
#
|
|
2846
|
-
local hooks_json
|
|
2847
|
-
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
3033
|
+
# Fresh install: generate settings.json from scratch
|
|
2848
3034
|
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
with open(
|
|
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:
|
|
2855
3041
|
json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
|
|
2856
3042
|
f.write('\n')
|
|
2857
|
-
"
|
|
3043
|
+
" "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
2858
3044
|
echo " Generated settings.json from hooks/compose.yml"
|
|
2859
3045
|
else
|
|
2860
3046
|
cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
2861
3047
|
fi
|
|
2862
3048
|
else
|
|
2863
|
-
|
|
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:-}"
|
|
2864
3102
|
fi
|
|
2865
3103
|
|
|
2866
3104
|
PROJECT_ROOT="$(dirname "$TARGET")"
|
|
@@ -2897,11 +3135,12 @@ with open('$TARGET/settings.json', 'w') as f:
|
|
|
2897
3135
|
|
|
2898
3136
|
run_audit "$(dirname "$TARGET")"
|
|
2899
3137
|
update_gitignore "$(dirname "$TARGET")" ".claude"
|
|
3138
|
+
setup_templates "$(dirname "$TARGET")"
|
|
2900
3139
|
|
|
2901
3140
|
echo ""
|
|
2902
3141
|
echo "CortexHawk installed successfully for Claude Code!"
|
|
2903
3142
|
echo ""
|
|
2904
|
-
echo "
|
|
3143
|
+
echo " 33 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
|
|
2905
3144
|
echo ""
|
|
2906
3145
|
do_quickstart
|
|
2907
3146
|
echo ""
|
|
@@ -3163,7 +3402,8 @@ do_test_hooks() {
|
|
|
3163
3402
|
echo ""
|
|
3164
3403
|
|
|
3165
3404
|
local ok=0 fail=0
|
|
3166
|
-
local tmpfile
|
|
3405
|
+
local tmpfile
|
|
3406
|
+
tmpfile=$(mktemp /tmp/.cortexhawk-hooktest-XXXXXX)
|
|
3167
3407
|
echo "test file content" > "$tmpfile"
|
|
3168
3408
|
|
|
3169
3409
|
for hook in "$hooks_dir"/*.sh; do
|
|
@@ -3501,9 +3741,11 @@ do_publish_skill() {
|
|
|
3501
3741
|
# Create GitHub repo
|
|
3502
3742
|
echo ""
|
|
3503
3743
|
echo " Creating repository..."
|
|
3744
|
+
local repo_exists=false
|
|
3504
3745
|
if gh repo view "$gh_user/$repo_name" &>/dev/null; then
|
|
3505
3746
|
echo " Repository already exists: $gh_user/$repo_name"
|
|
3506
3747
|
echo " Pushing latest files..."
|
|
3748
|
+
repo_exists=true
|
|
3507
3749
|
else
|
|
3508
3750
|
gh repo create "$repo_name" --public --description "CortexHawk skill: $skill_desc" --clone=false
|
|
3509
3751
|
fi
|
|
@@ -3527,7 +3769,11 @@ do_publish_skill() {
|
|
|
3527
3769
|
git commit --quiet -m "feat: publish $skill_name skill"
|
|
3528
3770
|
git branch -M main
|
|
3529
3771
|
git remote add origin "https://github.com/$gh_user/$repo_name.git"
|
|
3530
|
-
|
|
3772
|
+
if [ "$repo_exists" = true ]; then
|
|
3773
|
+
git push -u origin main --quiet 2>/dev/null
|
|
3774
|
+
else
|
|
3775
|
+
git push -u origin main --force --quiet 2>/dev/null
|
|
3776
|
+
fi
|
|
3531
3777
|
cd - >/dev/null
|
|
3532
3778
|
|
|
3533
3779
|
# Cleanup
|
|
@@ -3562,7 +3808,8 @@ do_publish_skill() {
|
|
|
3562
3808
|
|
|
3563
3809
|
# --- do_demo() ---
|
|
3564
3810
|
do_demo() {
|
|
3565
|
-
local demo_dir
|
|
3811
|
+
local demo_dir
|
|
3812
|
+
demo_dir=$(mktemp -d /tmp/cortexhawk-demo-XXXXXX)
|
|
3566
3813
|
mkdir -p "$demo_dir"
|
|
3567
3814
|
|
|
3568
3815
|
echo ""
|
|
@@ -3749,7 +3996,6 @@ else
|
|
|
3749
3996
|
install_codex
|
|
3750
3997
|
;;
|
|
3751
3998
|
auto)
|
|
3752
|
-
local detected
|
|
3753
3999
|
detected=$(detect_installed_clis)
|
|
3754
4000
|
if [ -z "$detected" ]; then
|
|
3755
4001
|
echo "Error: no supported CLI found (claude, kimi, codex)"
|
|
@@ -3758,7 +4004,7 @@ else
|
|
|
3758
4004
|
fi
|
|
3759
4005
|
echo "Auto-detected CLIs: $detected"
|
|
3760
4006
|
echo ""
|
|
3761
|
-
|
|
4007
|
+
auto_count=0
|
|
3762
4008
|
for cli in $detected; do
|
|
3763
4009
|
[ $auto_count -gt 0 ] && echo "" && echo "---" && echo ""
|
|
3764
4010
|
case "$cli" in
|