cortexhawk 3.1.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.
Files changed (136) hide show
  1. package/.cortexhawk-team.yml +65 -0
  2. package/CHANGELOG.md +268 -0
  3. package/CLAUDE.md +96 -0
  4. package/LICENSE +21 -0
  5. package/PACKS.md +14 -0
  6. package/README.md +418 -0
  7. package/REGISTRY.md +23 -0
  8. package/agents/architect.md +46 -0
  9. package/agents/brainstormer.md +57 -0
  10. package/agents/code-simplifier.md +56 -0
  11. package/agents/codebase-mapper.md +63 -0
  12. package/agents/copywriter.md +48 -0
  13. package/agents/debugger.md +44 -0
  14. package/agents/designer.md +53 -0
  15. package/agents/devops.md +49 -0
  16. package/agents/docs-manager.md +50 -0
  17. package/agents/fullstack-developer.md +55 -0
  18. package/agents/git-manager.md +63 -0
  19. package/agents/implementer.md +30 -0
  20. package/agents/journal-writer.md +53 -0
  21. package/agents/planner.md +52 -0
  22. package/agents/project-manager.md +50 -0
  23. package/agents/researcher.md +46 -0
  24. package/agents/reviewer.md +63 -0
  25. package/agents/security-auditor.md +92 -0
  26. package/agents/teacher.md +71 -0
  27. package/agents/tester.md +41 -0
  28. package/commands/api-gen.md +17 -0
  29. package/commands/backlog.md +26 -0
  30. package/commands/bootstrap.md +32 -0
  31. package/commands/brainstorm.md +18 -0
  32. package/commands/build.md +16 -0
  33. package/commands/chain.md +46 -0
  34. package/commands/changelog.md +16 -0
  35. package/commands/check.md +40 -0
  36. package/commands/ci.md +32 -0
  37. package/commands/context.md +35 -0
  38. package/commands/debug.md +16 -0
  39. package/commands/deploy.md +16 -0
  40. package/commands/doc.md +15 -0
  41. package/commands/export.md +17 -0
  42. package/commands/journal.md +18 -0
  43. package/commands/learn.md +16 -0
  44. package/commands/map.md +16 -0
  45. package/commands/migrate.md +17 -0
  46. package/commands/monitor.md +16 -0
  47. package/commands/optimize.md +17 -0
  48. package/commands/plan.md +17 -0
  49. package/commands/pulse.md +46 -0
  50. package/commands/refactor.md +16 -0
  51. package/commands/research.md +18 -0
  52. package/commands/review.md +16 -0
  53. package/commands/scan.md +19 -0
  54. package/commands/ship.md +17 -0
  55. package/commands/simplify.md +16 -0
  56. package/commands/task.md +32 -0
  57. package/commands/tdd.md +17 -0
  58. package/commands/test.md +16 -0
  59. package/commands/upgrade.md +27 -0
  60. package/cortexhawk +450 -0
  61. package/hooks/agent-analytics.sh +67 -0
  62. package/hooks/branch-guard.sh +56 -0
  63. package/hooks/codex-dispatcher.sh +84 -0
  64. package/hooks/commit-guard.sh +71 -0
  65. package/hooks/compose.yml +47 -0
  66. package/hooks/dependency-check.sh +56 -0
  67. package/hooks/file-guard.sh +69 -0
  68. package/hooks/hooks.json +46 -0
  69. package/hooks/self-review.sh +71 -0
  70. package/hooks/session-start.sh +132 -0
  71. package/hooks/session-telemetry.sh +60 -0
  72. package/hooks/test-reminder.sh +75 -0
  73. package/install.sh +3805 -0
  74. package/mcp/README.md +37 -0
  75. package/mcp/context7.json +8 -0
  76. package/mcp/puppeteer.json +8 -0
  77. package/mcp/sequential-thinking.json +8 -0
  78. package/modes/default.md +5 -0
  79. package/modes/fast.md +5 -0
  80. package/modes/learn.md +9 -0
  81. package/modes/orchestration.md +5 -0
  82. package/modes/pair.md +10 -0
  83. package/modes/research.md +5 -0
  84. package/modes/review.md +5 -0
  85. package/package.json +32 -0
  86. package/profiles/api.json +27 -0
  87. package/profiles/data.json +23 -0
  88. package/profiles/fullstack.json +27 -0
  89. package/scripts/autodetect-profile.sh +68 -0
  90. package/scripts/benchmark.sh +106 -0
  91. package/scripts/chain-post-save.sh +23 -0
  92. package/scripts/generate-plans-index.sh +50 -0
  93. package/scripts/git-workflow-init.sh +115 -0
  94. package/scripts/install-codex.sh +128 -0
  95. package/scripts/interactive-init.sh +264 -0
  96. package/scripts/post-install-audit.sh +130 -0
  97. package/scripts/validate.sh +214 -0
  98. package/settings.json +90 -0
  99. package/setup.sh +67 -0
  100. package/skills/databases/schema-designer/SKILL.md +54 -0
  101. package/skills/databases/sql-optimizer/SKILL.md +37 -0
  102. package/skills/devops/ci-cd/SKILL.md +59 -0
  103. package/skills/devops/deployment/SKILL.md +49 -0
  104. package/skills/devops/docker/SKILL.md +57 -0
  105. package/skills/frameworks/api-design/SKILL.md +103 -0
  106. package/skills/frameworks/fastapi/SKILL.md +68 -0
  107. package/skills/frameworks/nextjs/SKILL.md +74 -0
  108. package/skills/frameworks/python/SKILL.md +89 -0
  109. package/skills/frameworks/react/SKILL.md +83 -0
  110. package/skills/frameworks/sveltekit/SKILL.md +69 -0
  111. package/skills/frameworks/tailwindcss/SKILL.md +75 -0
  112. package/skills/frameworks/typescript/SKILL.md +94 -0
  113. package/skills/meta/mcp-builder/SKILL.md +54 -0
  114. package/skills/meta/skill-creator/SKILL.md +43 -0
  115. package/skills/optimization/performance/SKILL.md +70 -0
  116. package/skills/quality/complexity-analyzer/SKILL.md +52 -0
  117. package/skills/quality/error-handling/SKILL.md +123 -0
  118. package/skills/quality/log-analyzer/SKILL.md +31 -0
  119. package/skills/quality/pattern-detector/SKILL.md +50 -0
  120. package/skills/security/auth-analyzer/SKILL.md +96 -0
  121. package/skills/security/compliance-checker/SKILL.md +92 -0
  122. package/skills/security/container-security/SKILL.md +128 -0
  123. package/skills/security/dependency-auditor/SKILL.md +100 -0
  124. package/skills/security/encryption/SKILL.md +94 -0
  125. package/skills/security/incident-response/SKILL.md +127 -0
  126. package/skills/security/secrets/SKILL.md +93 -0
  127. package/skills/security/security-headers/SKILL.md +83 -0
  128. package/skills/security/security-logging/SKILL.md +107 -0
  129. package/skills/security/vulnerability-scanner/SKILL.md +114 -0
  130. package/skills/testing/e2e-testing/SKILL.md +119 -0
  131. package/skills/testing/tdd/SKILL.md +40 -0
  132. package/skills/testing/test-generator/SKILL.md +39 -0
  133. package/skills/workflow/commit/SKILL.md +61 -0
  134. package/skills/workflow/confidence-check/SKILL.md +90 -0
  135. package/skills/workflow/pr-review-comments/SKILL.md +81 -0
  136. package/skills/workflow/pr-review-comments/scripts/fetch_comments.py +237 -0
package/install.sh ADDED
@@ -0,0 +1,3805 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+
6
+ # --- Load .env if present (for SKILLSMP_API_KEY etc.) ---
7
+ if [ -f ".env" ]; then
8
+ while IFS='=' read -r key value; do
9
+ [[ -z "$key" || "$key" == \#* ]] && continue
10
+ value="${value%\"}" && value="${value#\"}"
11
+ export "$key=$value" 2>/dev/null || true
12
+ done < .env
13
+ fi
14
+
15
+ # --- Utility functions ---
16
+ green() { printf "\033[32m%s\033[0m\n" "$1"; }
17
+ yellow() { printf "\033[33m%s\033[0m\n" "$1"; }
18
+
19
+ get_version() {
20
+ grep -m1 '## \[' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
21
+ }
22
+
23
+ compute_checksum() {
24
+ local file="$1"
25
+ if command -v sha256sum >/dev/null 2>&1; then
26
+ sha256sum "$file" | cut -d' ' -f1
27
+ elif command -v shasum >/dev/null 2>&1; then
28
+ shasum -a 256 "$file" | cut -d' ' -f1
29
+ else
30
+ echo "error: no sha256 tool found" >&2
31
+ return 1
32
+ fi
33
+ }
34
+
35
+ detect_source_type() {
36
+ if [ -d "$SCRIPT_DIR/.git" ]; then
37
+ echo "git"
38
+ else
39
+ echo "release"
40
+ fi
41
+ }
42
+
43
+ get_source_url() {
44
+ if [ -n "${CORTEXHAWK_REPO:-}" ]; then
45
+ echo "$CORTEXHAWK_REPO"
46
+ return
47
+ fi
48
+ if [ -d "$SCRIPT_DIR/.git" ]; then
49
+ local url
50
+ url=$(git -C "$SCRIPT_DIR" remote get-url origin 2>/dev/null || true)
51
+ if [ -n "$url" ]; then
52
+ echo "$url" | sed 's|git@github.com:|https://github.com/|' | sed 's|\.git$||'
53
+ return
54
+ fi
55
+ fi
56
+ echo ""
57
+ }
58
+
59
+ # --- Usage ---
60
+ print_usage() {
61
+ echo "Usage: install.sh [--target claude|kimi|codex|auto|all] [--profile fullstack|api|data|autodetect] [--global]"
62
+ echo ""
63
+ echo "Targets:"
64
+ echo " claude (default) Install for Claude Code (.claude/) — fully supported"
65
+ echo " kimi Install for Kimi CLI (.kimi/) — experimental"
66
+ echo " codex Install for Codex CLI (.codex/ + .agents/) — experimental"
67
+ echo " auto Auto-detect installed CLIs and install for all found"
68
+ echo " all Install for all supported CLIs simultaneously"
69
+ echo ""
70
+ echo "Profiles:"
71
+ echo " fullstack Full-stack web development skills"
72
+ echo " api Backend API development skills"
73
+ echo " data Data engineering and data science skills"
74
+ echo " autodetect Scan project and install relevant skills"
75
+ echo " (none) Install all skills (default)"
76
+ echo ""
77
+ echo "Options:"
78
+ echo " --init Interactive setup wizard"
79
+ echo " --update Update existing installation (preserves customizations)"
80
+ echo " --force Force update even if already up to date"
81
+ echo " --snapshot Save current installation state to a snapshot file"
82
+ echo " --snapshot --portable Create portable tar.gz archive (self-contained)"
83
+ echo " --snapshots List all saved snapshots"
84
+ echo " --team Install from .cortexhawk-team.yml team config"
85
+ echo " --import-config Alias for --team (install from config YAML)"
86
+ echo " --export-team [file] Export snapshot/manifest to .cortexhawk-team.yml"
87
+ echo " --export-config [file] Alias for --export-team (export config YAML)"
88
+ echo " --diff [file] Compare current state against snapshot or manifest"
89
+ echo " --diff <a> <b> Semantic diff between two snapshots (metadata, skills, settings)"
90
+ echo " --restore <file> Restore installation from a snapshot file"
91
+ echo " --restore --latest Restore from the most recent snapshot"
92
+ echo " --add-skill <url> Install a community skill from GitHub (user/repo or full URL)"
93
+ echo " --search <keyword> Search skills (SkillsMP API if SKILLSMP_API_KEY set, else local REGISTRY.md)"
94
+ echo " --pack <name> Install a skill pack bundle (e.g., react-app, python-api, devops-full)"
95
+ echo " --list-hooks List all hooks with type, event, and enabled/disabled status"
96
+ echo " --enable-hook <n> Enable a disabled hook in compose.yml and regenerate settings.json"
97
+ echo " --disable-hook <n> Disable a hook in compose.yml and regenerate settings.json"
98
+ echo " --doctor Diagnose installation health (hooks, permissions, JSON, symlinks)"
99
+ echo " --uninstall Remove CortexHawk installation (creates snapshot first)"
100
+ echo " --global, -g Install to home directory instead of current project"
101
+ echo " --quickstart Show getting-started guide (5 things to try after install)"
102
+ echo " --stats Show installation stats (version, skills, hooks, agents, etc.)"
103
+ echo " --publish-skill <path> Publish a local skill to GitHub (requires gh CLI)"
104
+ echo " --check-update Check if a newer version is available and show what changed"
105
+ echo " --demo Create a sandbox project with CortexHawk pre-installed"
106
+ echo " --dry-run Simulate install/update without writing files"
107
+ echo " --test-hooks Dry-run all hooks with synthetic inputs"
108
+ echo " --no-scan Skip post-install security audit"
109
+ echo " --help, -h Show this help"
110
+ }
111
+
112
+ # --- Argument parsing ---
113
+ TARGET_CLI="claude"
114
+ PROFILE=""
115
+ PROFILE_FILE=""
116
+ GLOBAL=false
117
+ NO_SCAN=false
118
+ INIT_MODE=false
119
+ UPDATE_MODE=false
120
+ FORCE_MODE=false
121
+ SNAPSHOT_MODE=false
122
+ RESTORE_MODE=false
123
+ SNAPSHOTS_LIST=false
124
+ TEAM_MODE=false
125
+ DIFF_MODE=false
126
+ EXPORT_TEAM=false
127
+ SNAPSHOT_FILE=""
128
+ DIFF_FILE=""
129
+ DIFF_FILE2=""
130
+ EXPORT_TEAM_FILE=""
131
+ PORTABLE_MODE=false
132
+ ADD_SKILL_URL=""
133
+ SEARCH_KEYWORD=""
134
+ PACK_NAME=""
135
+ DOCTOR_MODE=false
136
+ UNINSTALL_MODE=false
137
+ LIST_HOOKS=false
138
+ ENABLE_HOOK=""
139
+ DISABLE_HOOK=""
140
+ DRY_RUN=false
141
+ TEST_HOOKS_MODE=false
142
+ QUICKSTART_MODE=false
143
+ STATS_MODE=false
144
+ PUBLISH_SKILL_PATH=""
145
+ CHECK_UPDATE_MODE=false
146
+ DEMO_MODE=false
147
+ MAX_SNAPSHOTS=10
148
+
149
+ while [ $# -gt 0 ]; do
150
+ case "$1" in
151
+ --target)
152
+ TARGET_CLI="$2"
153
+ shift 2
154
+ ;;
155
+ --profile)
156
+ PROFILE="$2"
157
+ shift 2
158
+ ;;
159
+ --global|-g)
160
+ GLOBAL=true
161
+ shift
162
+ ;;
163
+ --init)
164
+ INIT_MODE=true
165
+ shift
166
+ ;;
167
+ --update)
168
+ UPDATE_MODE=true
169
+ shift
170
+ ;;
171
+ --force)
172
+ FORCE_MODE=true
173
+ shift
174
+ ;;
175
+ --snapshot)
176
+ SNAPSHOT_MODE=true
177
+ shift
178
+ ;;
179
+ --portable)
180
+ PORTABLE_MODE=true
181
+ shift
182
+ ;;
183
+ --snapshots)
184
+ SNAPSHOTS_LIST=true
185
+ shift
186
+ ;;
187
+ --team)
188
+ TEAM_MODE=true
189
+ shift
190
+ ;;
191
+ --export-team|--export-config)
192
+ EXPORT_TEAM=true
193
+ if [ -n "${2:-}" ] && [[ "$2" != --* ]]; then
194
+ EXPORT_TEAM_FILE="$2"
195
+ shift 2
196
+ else
197
+ shift
198
+ fi
199
+ ;;
200
+ --import-config)
201
+ TEAM_MODE=true
202
+ shift
203
+ ;;
204
+ --diff)
205
+ DIFF_MODE=true
206
+ # Optional file argument(s): --diff [file1] [file2]
207
+ if [ -n "${2:-}" ] && [[ "$2" != --* ]]; then
208
+ DIFF_FILE="$2"
209
+ if [ -n "${3:-}" ] && [[ "$3" != --* ]]; then
210
+ DIFF_FILE2="$3"
211
+ shift 3
212
+ else
213
+ shift 2
214
+ fi
215
+ else
216
+ shift
217
+ fi
218
+ ;;
219
+ --restore)
220
+ RESTORE_MODE=true
221
+ SNAPSHOT_FILE="$2"
222
+ if [ "$SNAPSHOT_FILE" = "--latest" ]; then
223
+ if [ "$GLOBAL" = true ]; then
224
+ SNAP_DIR="$HOME/.claude/.cortexhawk-snapshots"
225
+ else
226
+ SNAP_DIR="$(pwd)/.claude/.cortexhawk-snapshots"
227
+ fi
228
+ SNAPSHOT_FILE=$(ls -t "$SNAP_DIR"/*.json 2>/dev/null | head -1)
229
+ if [ -z "$SNAPSHOT_FILE" ]; then
230
+ echo "Error: no snapshots found in $SNAP_DIR"
231
+ echo "Create one with: install.sh --snapshot"
232
+ exit 1
233
+ fi
234
+ echo "Using latest snapshot: $(basename "$SNAPSHOT_FILE")"
235
+ fi
236
+ shift 2
237
+ ;;
238
+ --no-scan)
239
+ NO_SCAN=true
240
+ shift
241
+ ;;
242
+ --doctor)
243
+ DOCTOR_MODE=true
244
+ shift
245
+ ;;
246
+ --uninstall)
247
+ UNINSTALL_MODE=true
248
+ shift
249
+ ;;
250
+ --dry-run)
251
+ DRY_RUN=true
252
+ shift
253
+ ;;
254
+ --test-hooks)
255
+ TEST_HOOKS_MODE=true
256
+ shift
257
+ ;;
258
+ --quickstart)
259
+ QUICKSTART_MODE=true
260
+ shift
261
+ ;;
262
+ --stats)
263
+ STATS_MODE=true
264
+ shift
265
+ ;;
266
+ --check-update)
267
+ CHECK_UPDATE_MODE=true
268
+ shift
269
+ ;;
270
+ --demo)
271
+ DEMO_MODE=true
272
+ shift
273
+ ;;
274
+ --publish-skill)
275
+ PUBLISH_SKILL_PATH="$2"
276
+ if [ -z "$PUBLISH_SKILL_PATH" ]; then
277
+ echo "Error: --publish-skill requires a path argument"
278
+ exit 1
279
+ fi
280
+ shift 2
281
+ ;;
282
+ --list-hooks)
283
+ LIST_HOOKS=true
284
+ shift
285
+ ;;
286
+ --enable-hook)
287
+ ENABLE_HOOK="$2"
288
+ if [ -z "$ENABLE_HOOK" ]; then
289
+ echo "Error: --enable-hook requires a hook name"
290
+ exit 1
291
+ fi
292
+ shift 2
293
+ ;;
294
+ --disable-hook)
295
+ DISABLE_HOOK="$2"
296
+ if [ -z "$DISABLE_HOOK" ]; then
297
+ echo "Error: --disable-hook requires a hook name"
298
+ exit 1
299
+ fi
300
+ shift 2
301
+ ;;
302
+ --add-skill)
303
+ ADD_SKILL_URL="$2"
304
+ if [ -z "$ADD_SKILL_URL" ]; then
305
+ echo "Error: --add-skill requires a GitHub URL or user/repo"
306
+ exit 1
307
+ fi
308
+ shift 2
309
+ ;;
310
+ --search)
311
+ SEARCH_KEYWORD="$2"
312
+ if [ -z "$SEARCH_KEYWORD" ]; then
313
+ echo "Error: --search requires a keyword"
314
+ exit 1
315
+ fi
316
+ shift 2
317
+ ;;
318
+ --pack)
319
+ PACK_NAME="$2"
320
+ if [ -z "$PACK_NAME" ]; then
321
+ echo "Error: --pack requires a pack name (e.g., --pack react-app)"
322
+ exit 1
323
+ fi
324
+ shift 2
325
+ ;;
326
+ --help|-h)
327
+ print_usage
328
+ exit 0
329
+ ;;
330
+ *)
331
+ echo "Unknown option: $1"
332
+ print_usage
333
+ exit 1
334
+ ;;
335
+ esac
336
+ done
337
+
338
+ # --- Mutual exclusion ---
339
+ if [ "$INIT_MODE" = true ] && [ "$UPDATE_MODE" = true ]; then
340
+ echo "Error: --init and --update are mutually exclusive"
341
+ exit 1
342
+ fi
343
+ if [ "$SNAPSHOT_MODE" = true ] && [ "$RESTORE_MODE" = true ]; then
344
+ echo "Error: --snapshot and --restore are mutually exclusive"
345
+ exit 1
346
+ fi
347
+ if [ "$SNAPSHOTS_LIST" = true ] && { [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ] || [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ]; }; then
348
+ echo "Error: --snapshots cannot be combined with other modes"
349
+ exit 1
350
+ fi
351
+ if [ "$TEAM_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
352
+ echo "Error: --team cannot be combined with --init, --update, --snapshot, or --restore"
353
+ exit 1
354
+ fi
355
+ if [ "$DIFF_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
356
+ echo "Error: --diff cannot be combined with --init, --update, --snapshot, or --restore"
357
+ exit 1
358
+ fi
359
+ if [ "$EXPORT_TEAM" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
360
+ echo "Error: --export-team cannot be combined with --init, --update, --snapshot, or --restore"
361
+ exit 1
362
+ fi
363
+ if [ "$SNAPSHOT_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ]; }; then
364
+ echo "Error: --snapshot cannot be combined with --init or --update"
365
+ exit 1
366
+ fi
367
+ if [ "$RESTORE_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ]; }; then
368
+ echo "Error: --restore cannot be combined with --init or --update"
369
+ exit 1
370
+ 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
+ if [ -n "$ADD_SKILL_URL" ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
380
+ echo "Error: --add-skill cannot be combined with --init, --update, --snapshot, or --restore"
381
+ exit 1
382
+ fi
383
+ if [ -n "$PACK_NAME" ] && [ -n "$PROFILE" ]; then
384
+ echo "Error: --pack cannot be combined with --profile (use one or the other)"
385
+ exit 1
386
+ fi
387
+ if [ -n "$SEARCH_KEYWORD" ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ -n "$ADD_SKILL_URL" ]; }; then
388
+ echo "Error: --search cannot be combined with --init, --update, or --add-skill"
389
+ exit 1
390
+ fi
391
+ if [ "$DOCTOR_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
392
+ echo "Error: --doctor cannot be combined with --init, --update, --snapshot, or --restore"
393
+ exit 1
394
+ fi
395
+ if [ "$UNINSTALL_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ] || [ "$DOCTOR_MODE" = true ]; }; then
396
+ echo "Error: --uninstall cannot be combined with other modes"
397
+ exit 1
398
+ fi
399
+ if [ "$DRY_RUN" = true ] && { [ "$DOCTOR_MODE" = true ] || [ "$UNINSTALL_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ] || [ "$DIFF_MODE" = true ] || [ "$EXPORT_TEAM" = true ] || [ "$SNAPSHOTS_LIST" = true ] || [ -n "$ADD_SKILL_URL" ] || [ "$TEAM_MODE" = true ]; }; then
400
+ echo "Error: --dry-run can only be combined with install or --update"
401
+ exit 1
402
+ fi
403
+ if [ "$TEST_HOOKS_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ] || [ "$DOCTOR_MODE" = true ] || [ "$UNINSTALL_MODE" = true ] || [ "$DRY_RUN" = true ]; }; then
404
+ echo "Error: --test-hooks cannot be combined with other modes"
405
+ exit 1
406
+ fi
407
+
408
+ # --- Interactive mode ---
409
+ if [ "$INIT_MODE" = true ]; then
410
+ if [ ! -f "$SCRIPT_DIR/scripts/interactive-init.sh" ]; then
411
+ echo "Error: interactive-init.sh not found"
412
+ exit 1
413
+ fi
414
+ source "$SCRIPT_DIR/scripts/interactive-init.sh"
415
+ fi
416
+
417
+ # --- Pack resolution ---
418
+ if [ -n "$PACK_NAME" ]; then
419
+ _packs_file="$SCRIPT_DIR/PACKS.md"
420
+ if [ ! -f "$_packs_file" ]; then
421
+ echo "Error: PACKS.md not found at $_packs_file"
422
+ exit 1
423
+ fi
424
+ _row=$(grep "^| ${PACK_NAME} |" "$_packs_file" || true)
425
+ if [ -z "$_row" ]; then
426
+ echo "Error: pack '$PACK_NAME' not found in PACKS.md"
427
+ echo ""
428
+ echo "Available packs:"
429
+ grep '^| [a-z]' "$_packs_file" | awk -F'|' '{printf " %-16s %s\n", $2, $4}'
430
+ exit 1
431
+ fi
432
+ _skills_csv=$(echo "$_row" | awk -F'|' '{print $3}' | sed 's/^ *//;s/ *$//')
433
+ _description=$(echo "$_row" | awk -F'|' '{print $4}' | sed 's/^ *//;s/ *$//')
434
+ _tmp_profile=$(mktemp /tmp/cortexhawk-pack-XXXXXX.json)
435
+ {
436
+ echo '{'
437
+ echo " \"name\": \"pack-${PACK_NAME}\","
438
+ echo " \"description\": \"${_description}\","
439
+ echo ' "skills": ['
440
+ _first=true
441
+ for _skill in $(echo "$_skills_csv" | tr ',' '\n' | sed 's/^ *//;s/ *$//'); do
442
+ [ "$_first" = true ] && _first=false || printf ',\n'
443
+ printf ' "%s"' "$_skill"
444
+ done
445
+ echo ''
446
+ echo ' ]'
447
+ echo '}'
448
+ } > "$_tmp_profile"
449
+ PROFILE="pack-${PACK_NAME}"
450
+ PROFILE_FILE="$_tmp_profile"
451
+ echo " Pack '$PACK_NAME': $_description"
452
+ fi
453
+
454
+ # --- Profile validation ---
455
+ if [ "$PROFILE" = "autodetect" ] && [ -z "$PROFILE_FILE" ]; then
456
+ source "$SCRIPT_DIR/scripts/autodetect-profile.sh"
457
+ fi
458
+ if [ -n "$PROFILE" ]; then
459
+ if [ -z "$PROFILE_FILE" ]; then
460
+ PROFILE_FILE="$SCRIPT_DIR/profiles/${PROFILE}.json"
461
+ fi
462
+ if [ ! -f "$PROFILE_FILE" ]; then
463
+ echo "Error: unknown profile '$PROFILE'"
464
+ echo "Available profiles: fullstack, api, data, autodetect"
465
+ exit 1
466
+ fi
467
+ fi
468
+
469
+ # --- detect_installed_clis() ---
470
+ # Returns space-separated list of installed CLI tools
471
+ detect_installed_clis() {
472
+ local found=()
473
+ command -v claude >/dev/null 2>&1 && found+=("claude")
474
+ command -v kimi >/dev/null 2>&1 && found+=("kimi")
475
+ command -v codex >/dev/null 2>&1 && found+=("codex")
476
+ echo "${found[*]}"
477
+ }
478
+
479
+ # --- copy_skills() ---
480
+ copy_skills() {
481
+ local target_dir="$1"
482
+ local profile="$2"
483
+ if [ -z "$profile" ]; then
484
+ cp -r "$SCRIPT_DIR/skills/"* "$target_dir/skills/" 2>/dev/null || true
485
+ else
486
+ grep '"[a-z]' "$PROFILE_FILE" | sed 's/.*"\([a-z][^"]*\)".*/\1/' | grep '/' | while read -r skill; do
487
+ local src="$SCRIPT_DIR/skills/$skill"
488
+ local dest="$target_dir/skills/$skill"
489
+ mkdir -p "$(dirname "$dest")"
490
+ cp -r "$src" "$dest" 2>/dev/null || true
491
+ done
492
+ echo " Profile '$profile': installed $(grep '/' "$PROFILE_FILE" | grep -c '"[a-z]') skills (out of 36)"
493
+ fi
494
+ }
495
+
496
+ # --- Shared conversion functions ---
497
+ # Used by install_claude(), install_kimi(), and install_codex()
498
+
499
+ # create_docs_workspace(project_root) — creates docs/ subdirectories for agent outputs
500
+ create_docs_workspace() {
501
+ local project_root="$1"
502
+ mkdir -p "$project_root/docs"/{brainstorms,plans,decisions,research,audits,conversations,chains,.context,.metrics}
503
+ echo " Created docs/ workspace"
504
+ }
505
+
506
+ # generate_agents_md(output_file, subtitle [, extra_content])
507
+ # Generates AGENTS.md from CLAUDE.md sections (Agents, Skills, Modes, Conventions)
508
+ # extra_content is inserted between Skills and Modes (e.g. Commands listing for Codex)
509
+ generate_agents_md() {
510
+ local output_file="$1"
511
+ local subtitle="$2"
512
+ local extra_content="${3:-}"
513
+ {
514
+ echo "# CortexHawk"
515
+ echo ""
516
+ echo "Development toolkit — $subtitle"
517
+ echo ""
518
+ awk '/^## Agents$/,/^## [^A]/' "$SCRIPT_DIR/CLAUDE.md" | sed '$d'
519
+ echo ""
520
+ awk '/^## Skills$/,/^## [^S]/' "$SCRIPT_DIR/CLAUDE.md" | sed '$d'
521
+ if [ -n "$extra_content" ]; then
522
+ echo ""
523
+ echo "$extra_content"
524
+ fi
525
+ echo ""
526
+ awk '/^## Modes$/,/^## [^M]/' "$SCRIPT_DIR/CLAUDE.md" | sed '$d'
527
+ echo ""
528
+ awk '/^## Conventions$/,/^$/' "$SCRIPT_DIR/CLAUDE.md"
529
+ } > "$output_file"
530
+ }
531
+
532
+ # convert_modes_to_skills(skills_dir, prefix)
533
+ # Copies modes/*.md into skills dir as SKILL.md
534
+ # prefix: "modes/" for Kimi (→ skills/modes/fast/), "mode-" for Codex (→ skills/mode-fast/)
535
+ convert_modes_to_skills() {
536
+ local skills_dir="$1"
537
+ local prefix="$2"
538
+ echo " Converting modes to skills..."
539
+ for mode_file in "$SCRIPT_DIR"/modes/*.md; do
540
+ [ -f "$mode_file" ] || continue
541
+ local mode_name
542
+ mode_name=$(basename "$mode_file" .md)
543
+ mkdir -p "$skills_dir/${prefix}${mode_name}"
544
+ cp "$mode_file" "$skills_dir/${prefix}${mode_name}/SKILL.md"
545
+ done
546
+ }
547
+
548
+ # merge_mcp_json(output_file) — merges mcp/*.json into single JSON file
549
+ merge_mcp_json() {
550
+ local output_file="$1"
551
+ local entries=""
552
+ for mcp_file in "$SCRIPT_DIR"/mcp/*.json; do
553
+ [ -f "$mcp_file" ] || continue
554
+ local server_name command_val args_val entry
555
+ server_name=$(basename "$mcp_file" .json)
556
+ command_val=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
557
+ args_val=$(grep '"args"' "$mcp_file" | sed 's/.*"args"[[:space:]]*:[[:space:]]*\(\[.*\]\).*/\1/')
558
+ entry=" \"$server_name\": {\n \"command\": \"$command_val\",\n \"args\": $args_val\n }"
559
+ if [ -n "$entries" ]; then
560
+ entries="$entries,\n$entry"
561
+ else
562
+ entries="$entry"
563
+ fi
564
+ done
565
+ printf '{\n "mcpServers": {\n'"$entries"'\n }\n}\n' > "$output_file"
566
+ }
567
+
568
+ # merge_mcp_toml() — outputs TOML [mcp.servers.*] sections to stdout
569
+ merge_mcp_toml() {
570
+ for mcp_file in "$SCRIPT_DIR"/mcp/*.json; do
571
+ [ -f "$mcp_file" ] || continue
572
+ local server_name command_val args_val
573
+ server_name=$(basename "$mcp_file" .json)
574
+ command_val=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
575
+ args_val=$(grep '"args"' "$mcp_file" | sed 's/.*"args"[[:space:]]*:[[:space:]]*\(\[.*\]\).*/\1/')
576
+ echo "[mcp.servers.$server_name]"
577
+ echo "command = \"$command_val\""
578
+ echo "args = $args_val"
579
+ echo ""
580
+ done
581
+ }
582
+
583
+ # --- update_gitignore() ---
584
+ # Args: $1=project_root $2=target_dir_name (e.g., .claude, .kimi)
585
+ update_gitignore() {
586
+ local project_root="$1"
587
+ local target_dir="$2"
588
+ local gitignore="$project_root/.gitignore"
589
+
590
+ [ "$GLOBAL" = true ] && return
591
+
592
+ # Create .gitignore if it doesn't exist
593
+ if [ ! -f "$gitignore" ]; then
594
+ touch "$gitignore"
595
+ fi
596
+
597
+ # Auto-add target directory (always)
598
+ if ! grep -qx "$target_dir/" "$gitignore" 2>/dev/null; then
599
+ echo "" >> "$gitignore"
600
+ echo "# CortexHawk" >> "$gitignore"
601
+ echo "$target_dir/" >> "$gitignore"
602
+ green " Added $target_dir/ to .gitignore"
603
+ fi
604
+
605
+ # Ask about docs/ (only if docs/ exists and not already in .gitignore)
606
+ if [ -d "$project_root/docs" ] && ! grep -qx "docs/" "$gitignore" 2>/dev/null; then
607
+ echo ""
608
+ echo " docs/ contains agent outputs (brainstorms, plans, research)."
609
+ read -r -p " Track docs/ in git? [Y/n]: " docs_choice
610
+ case "$docs_choice" in
611
+ [nN]*)
612
+ echo "docs/" >> "$gitignore"
613
+ green " Added docs/ to .gitignore"
614
+ ;;
615
+ *)
616
+ green " docs/ will be tracked in git"
617
+ ;;
618
+ esac
619
+ fi
620
+ }
621
+
622
+ # --- run_audit() ---
623
+ run_audit() {
624
+ local project_root="$1"
625
+ if [ "$NO_SCAN" = true ]; then return 0; fi
626
+ if [ -f "$SCRIPT_DIR/scripts/post-install-audit.sh" ]; then
627
+ bash "$SCRIPT_DIR/scripts/post-install-audit.sh" "$project_root" || true
628
+ fi
629
+ }
630
+
631
+ # --- generate_hooks_config() ---
632
+ # Parses hooks/compose.yml and generates JSON hooks config for settings.json
633
+ # Args: $1 = compose.yml path, $2 = hooks dir prefix (e.g., .claude/hooks)
634
+ # Outputs JSON to stdout
635
+ generate_hooks_config() {
636
+ local compose_file="$1"
637
+ local hooks_dir="$2"
638
+
639
+ if [ ! -f "$compose_file" ]; then
640
+ return 1
641
+ fi
642
+
643
+ if ! command -v python3 >/dev/null 2>&1; then
644
+ return 1
645
+ fi
646
+
647
+ python3 << PYEOF
648
+ import re, json, sys
649
+
650
+ def parse_compose(path, hooks_dir):
651
+ """Parse compose.yml and generate Claude Code hooks JSON."""
652
+ events = {"SessionStart": [], "PreToolUse": [], "PostToolUse": [], "SessionEnd": []}
653
+
654
+ with open(path) as f:
655
+ content = f.read()
656
+
657
+ # Simple YAML parser for our specific format
658
+ current_comp = None
659
+ current_event = None
660
+ current_matcher = None
661
+
662
+ for line in content.split('\n'):
663
+ line = line.rstrip()
664
+ if not line or line.strip().startswith('#'):
665
+ continue
666
+
667
+ # New composition (2 spaces indent)
668
+ if re.match(r'^ [a-z]', line) and ':' in line and 'event' not in line:
669
+ current_comp = line.strip().rstrip(':')
670
+ current_event = None
671
+ current_matcher = None
672
+ # Event
673
+ elif 'event:' in line:
674
+ current_event = line.split('event:')[1].strip()
675
+ # Matcher
676
+ elif 'matcher:' in line:
677
+ m = line.split('matcher:')[1].strip().strip('"').strip("'")
678
+ current_matcher = m
679
+ # Hook item
680
+ elif line.strip().startswith('- '):
681
+ hook_name = line.strip()[2:].strip()
682
+ hook_path = f"{hooks_dir}/{hook_name}.sh"
683
+
684
+ # Build command — hooks read stdin JSON (Claude Code protocol)
685
+ cmd = f'bash {hook_path}'
686
+
687
+ hook_entry = {"type": "command", "command": cmd}
688
+
689
+ # Find or create matcher group
690
+ found = False
691
+ for group in events[current_event]:
692
+ if group["matcher"] == current_matcher:
693
+ group["hooks"].append(hook_entry)
694
+ found = True
695
+ break
696
+ if not found:
697
+ events[current_event].append({
698
+ "matcher": current_matcher,
699
+ "hooks": [hook_entry]
700
+ })
701
+
702
+ # Build final structure
703
+ result = {}
704
+ for event, groups in events.items():
705
+ if groups:
706
+ result[event] = groups
707
+
708
+ return result
709
+
710
+ try:
711
+ result = parse_compose("$compose_file", "$hooks_dir")
712
+ print(json.dumps(result, indent=2))
713
+ except Exception as e:
714
+ print(f"Error: {e}", file=sys.stderr)
715
+ sys.exit(1)
716
+ PYEOF
717
+ }
718
+
719
+ # --- write_manifest() ---
720
+ write_manifest() {
721
+ local target_dir="$1"
722
+ local profile="$2"
723
+ local target_cli="$3"
724
+ local is_update="${4:-false}"
725
+ local manifest="$target_dir/.cortexhawk-manifest"
726
+ local version
727
+ version=$(get_version)
728
+ local now
729
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
730
+ local install_date="$now"
731
+ local source_type
732
+ source_type=$(detect_source_type)
733
+ local source_url
734
+ source_url=$(get_source_url)
735
+
736
+ # Preserve install_date on update
737
+ if [ "$is_update" = true ] && [ -f "$manifest" ]; then
738
+ local old_date
739
+ old_date=$(grep '"install_date"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
740
+ [ -n "$old_date" ] && install_date="$old_date"
741
+ fi
742
+
743
+ # Build files JSON
744
+ local files_json=""
745
+ local first=true
746
+ while IFS= read -r file; do
747
+ local relpath="${file#$target_dir/}"
748
+ case "$relpath" in
749
+ settings.json|git-workflow.conf|.cortexhawk-manifest) continue ;;
750
+ esac
751
+ local checksum
752
+ checksum=$(compute_checksum "$file")
753
+ if [ "$first" = true ]; then
754
+ files_json=$(printf ' "%s": "sha256:%s"' "$relpath" "$checksum")
755
+ first=false
756
+ else
757
+ files_json=$(printf '%s,\n "%s": "sha256:%s"' "$files_json" "$relpath" "$checksum")
758
+ fi
759
+ done < <(find "$target_dir" -type f | sort)
760
+
761
+ printf '{\n "version": "%s",\n "install_date": "%s",\n "update_date": "%s",\n "profile": "%s",\n "target": "%s",\n "source": "%s",\n "source_url": "%s",\n "source_path": "%s",\n "files": {\n%s\n }\n}\n' \
762
+ "$version" "$install_date" "$now" "${profile:-all}" "$target_cli" \
763
+ "$source_type" "$source_url" "$SCRIPT_DIR" "$files_json" \
764
+ > "$manifest"
765
+ }
766
+
767
+ # --- Update functions ---
768
+ UPDATE_CLEANUP_DIR=""
769
+
770
+ cleanup_update() {
771
+ if [ -n "$UPDATE_CLEANUP_DIR" ]; then
772
+ rm -rf "$UPDATE_CLEANUP_DIR"
773
+ fi
774
+ }
775
+
776
+ update_source_git() {
777
+ local current_branch
778
+ current_branch=$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
779
+ if [ "$current_branch" != "main" ]; then
780
+ echo " Warning: source repo is on branch '$current_branch', not 'main'"
781
+ fi
782
+ if ! git -C "$SCRIPT_DIR" pull --ff-only 2>&1; then
783
+ echo "Error: git pull failed. Resolve conflicts in $SCRIPT_DIR and retry."
784
+ exit 1
785
+ fi
786
+ }
787
+
788
+ update_source_release() {
789
+ local tmp_dir="/tmp/cortexhawk-update-$$"
790
+ local repo_url
791
+ repo_url=$(get_source_url)
792
+ if [ -z "$repo_url" ] && [ -f "$TARGET/.cortexhawk-manifest" ]; then
793
+ repo_url=$(grep '"source_url"' "$TARGET/.cortexhawk-manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
794
+ fi
795
+ if [ -z "$repo_url" ]; then
796
+ echo "Error: cannot determine source repository URL"
797
+ echo "Set CORTEXHAWK_REPO environment variable and retry"
798
+ exit 1
799
+ fi
800
+ mkdir -p "$tmp_dir"
801
+ echo " Downloading from $repo_url..."
802
+ if ! curl -sL "$repo_url/archive/refs/heads/main.tar.gz" | tar xz -C "$tmp_dir" --strip-components=1; then
803
+ echo "Error: failed to download latest release"
804
+ rm -rf "$tmp_dir"
805
+ exit 1
806
+ fi
807
+ SCRIPT_DIR="$tmp_dir"
808
+ UPDATE_CLEANUP_DIR="$tmp_dir"
809
+ }
810
+
811
+ # Sync counters (global for use across functions)
812
+ SYNC_ADDED=0
813
+ SYNC_UPDATED=0
814
+ SYNC_UNCHANGED=0
815
+ SYNC_SKIPPED=0
816
+ SYNC_CONFLICTS=0
817
+
818
+ prompt_conflict() {
819
+ local relpath="$1"
820
+ local target_file="$2"
821
+ local source_file="$3"
822
+
823
+ SYNC_CONFLICTS=$((SYNC_CONFLICTS + 1))
824
+
825
+ # Non-interactive mode — skip conflicts silently
826
+ if [ ! -e /dev/tty ]; then
827
+ echo " CONFLICT: $relpath (skipped — non-interactive)"
828
+ SYNC_SKIPPED=$((SYNC_SKIPPED + 1))
829
+ return 0
830
+ fi
831
+
832
+ echo ""
833
+ echo " CONFLICT: $relpath"
834
+ echo " You have modified this file, and a new version is available."
835
+ echo ""
836
+ echo " [o] Overwrite with new version"
837
+ echo " [s] Skip — keep your version"
838
+ echo " [d] Show diff"
839
+ echo ""
840
+
841
+ while true; do
842
+ printf " Choice [o/s/d] (default: s): "
843
+ read -r choice </dev/tty
844
+ case "${choice:-s}" in
845
+ o)
846
+ cp "$source_file" "$target_file"
847
+ SYNC_UPDATED=$((SYNC_UPDATED + 1))
848
+ return 0
849
+ ;;
850
+ s)
851
+ SYNC_SKIPPED=$((SYNC_SKIPPED + 1))
852
+ return 0
853
+ ;;
854
+ d)
855
+ echo ""
856
+ diff "$target_file" "$source_file" || true
857
+ echo ""
858
+ ;;
859
+ *)
860
+ echo " Invalid choice. Please enter o, s, or d."
861
+ ;;
862
+ esac
863
+ done
864
+ }
865
+
866
+ sync_file() {
867
+ local source_file="$1"
868
+ local target_file="$2"
869
+ local manifest="$3"
870
+ local relpath="$4"
871
+
872
+ local source_checksum
873
+ source_checksum=$(compute_checksum "$source_file")
874
+
875
+ # New file — copy directly
876
+ if [ ! -f "$target_file" ]; then
877
+ if [ "$DRY_RUN" != true ]; then
878
+ mkdir -p "$(dirname "$target_file")"
879
+ cp "$source_file" "$target_file"
880
+ else
881
+ echo " + $relpath"
882
+ fi
883
+ SYNC_ADDED=$((SYNC_ADDED + 1))
884
+ return 0
885
+ fi
886
+
887
+ local current_checksum
888
+ current_checksum=$(compute_checksum "$target_file")
889
+
890
+ # Source identical to current — nothing to do
891
+ if [ "$current_checksum" = "$source_checksum" ]; then
892
+ SYNC_UNCHANGED=$((SYNC_UNCHANGED + 1))
893
+ return 0
894
+ fi
895
+
896
+ # Get manifest checksum (from last install/update)
897
+ local manifest_checksum=""
898
+ if [ -f "$manifest" ]; then
899
+ manifest_checksum=$(grep -F "\"$relpath\": \"sha256:" "$manifest" 2>/dev/null | sed 's/.*"sha256:\([^"]*\)".*/\1/' || true)
900
+ fi
901
+
902
+ # No manifest entry — no baseline, treat as conflict
903
+ if [ -z "$manifest_checksum" ]; then
904
+ if [ "$DRY_RUN" = true ]; then
905
+ echo " ? $relpath (conflict — no baseline)"
906
+ SYNC_CONFLICTS=$((SYNC_CONFLICTS + 1))
907
+ else
908
+ prompt_conflict "$relpath" "$target_file" "$source_file"
909
+ fi
910
+ return 0
911
+ fi
912
+
913
+ local manifest_eq_current=false
914
+ local manifest_eq_source=false
915
+ [ "$manifest_checksum" = "$current_checksum" ] && manifest_eq_current=true
916
+ [ "$manifest_checksum" = "$source_checksum" ] && manifest_eq_source=true
917
+
918
+ if [ "$manifest_eq_current" = true ] && [ "$manifest_eq_source" = true ]; then
919
+ # Nothing changed anywhere
920
+ SYNC_UNCHANGED=$((SYNC_UNCHANGED + 1))
921
+ elif [ "$manifest_eq_current" = true ] && [ "$manifest_eq_source" = false ]; then
922
+ # User didn't touch, source changed — overwrite silently
923
+ if [ "$DRY_RUN" = true ]; then
924
+ echo " ~ $relpath"
925
+ else
926
+ cp "$source_file" "$target_file"
927
+ fi
928
+ SYNC_UPDATED=$((SYNC_UPDATED + 1))
929
+ elif [ "$manifest_eq_current" = false ] && [ "$manifest_eq_source" = true ]; then
930
+ # User modified, source unchanged — skip
931
+ [ "$DRY_RUN" = true ] && echo " s $relpath (user modified)"
932
+ SYNC_SKIPPED=$((SYNC_SKIPPED + 1))
933
+ else
934
+ # Both changed — conflict
935
+ if [ "$DRY_RUN" = true ]; then
936
+ echo " ? $relpath (conflict)"
937
+ SYNC_CONFLICTS=$((SYNC_CONFLICTS + 1))
938
+ else
939
+ prompt_conflict "$relpath" "$target_file" "$source_file"
940
+ fi
941
+ fi
942
+ }
943
+
944
+ sync_component() {
945
+ local component="$1"
946
+ local source_dir="$SCRIPT_DIR/$component"
947
+ local target_dir="$TARGET/$component"
948
+ local manifest="$TARGET/.cortexhawk-manifest"
949
+
950
+ [ "$DRY_RUN" != true ] && echo "Syncing $component..."
951
+
952
+ [ -d "$source_dir" ] || return 0
953
+ [ "$DRY_RUN" != true ] && mkdir -p "$target_dir"
954
+
955
+ while IFS= read -r source_file; do
956
+ local relpath="${source_file#$source_dir/}"
957
+ local target_file="$target_dir/$relpath"
958
+ sync_file "$source_file" "$target_file" "$manifest" "$component/$relpath"
959
+ done < <(find "$source_dir" -type f | sort)
960
+ }
961
+
962
+ sync_skills_update() {
963
+ local update_profile="$1"
964
+ local source_dir="$SCRIPT_DIR/skills"
965
+ local target_dir="$TARGET/skills"
966
+ local manifest="$TARGET/.cortexhawk-manifest"
967
+
968
+ [ "$DRY_RUN" != true ] && echo "Syncing skills..."
969
+
970
+ [ "$DRY_RUN" != true ] && mkdir -p "$target_dir"
971
+
972
+ if [ -z "$update_profile" ] || [ "$update_profile" = "all" ]; then
973
+ while IFS= read -r source_file; do
974
+ local relpath="${source_file#$source_dir/}"
975
+ local target_file="$target_dir/$relpath"
976
+ sync_file "$source_file" "$target_file" "$manifest" "skills/$relpath"
977
+ done < <(find "$source_dir" -type f | sort)
978
+ else
979
+ local skill_list
980
+ skill_list=$(grep '"[a-z]' "$PROFILE_FILE" | sed 's/.*"\([a-z][^"]*\)".*/\1/' | grep '/')
981
+ while IFS= read -r skill; do
982
+ [ -z "$skill" ] && continue
983
+ local src="$source_dir/$skill"
984
+ [ -e "$src" ] || continue
985
+ if [ -f "$src" ]; then
986
+ sync_file "$src" "$target_dir/$skill" "$manifest" "skills/$skill"
987
+ elif [ -d "$src" ]; then
988
+ while IFS= read -r source_file; do
989
+ local relpath="${source_file#$source_dir/}"
990
+ local target_file="$target_dir/$relpath"
991
+ sync_file "$source_file" "$target_file" "$manifest" "skills/$relpath"
992
+ done < <(find "$src" -type f | sort)
993
+ fi
994
+ done <<< "$skill_list"
995
+ fi
996
+ }
997
+
998
+ do_update() {
999
+ # 1. Validate target
1000
+ if [ "$TARGET_CLI" != "claude" ]; then
1001
+ echo "Error: --update is currently supported for Claude Code only"
1002
+ echo "For Kimi CLI, re-run: install.sh --target kimi"
1003
+ exit 1
1004
+ fi
1005
+
1006
+ if [ "$GLOBAL" = true ]; then
1007
+ TARGET="$HOME/.claude"
1008
+ else
1009
+ TARGET="$(pwd)/.claude"
1010
+ fi
1011
+
1012
+ if [ ! -d "$TARGET" ]; then
1013
+ echo "Error: no CortexHawk installation found at $TARGET"
1014
+ echo "Run install.sh without --update for a fresh install"
1015
+ exit 1
1016
+ fi
1017
+
1018
+ # 2. Read manifest
1019
+ local manifest="$TARGET/.cortexhawk-manifest"
1020
+ local current_version="unknown"
1021
+ local current_profile="all"
1022
+ local source_type
1023
+ source_type=$(detect_source_type)
1024
+
1025
+ if [ -f "$manifest" ]; then
1026
+ current_version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1027
+ current_profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1028
+ local manifest_source
1029
+ manifest_source=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1030
+ [ -n "$manifest_source" ] && source_type="$manifest_source"
1031
+ else
1032
+ echo " No manifest found — treating as pre-update installation"
1033
+ fi
1034
+
1035
+ # 3. Profile override
1036
+ local update_profile="$current_profile"
1037
+ if [ -n "$PROFILE" ]; then
1038
+ update_profile="$PROFILE"
1039
+ fi
1040
+
1041
+ if [ "$DRY_RUN" = true ]; then
1042
+ echo "CortexHawk Dry Run (update)"
1043
+ echo "============================"
1044
+ else
1045
+ echo "CortexHawk Update"
1046
+ echo "==================="
1047
+ fi
1048
+ echo " Current version: $current_version"
1049
+ echo " Profile: $update_profile"
1050
+ echo " Source: $source_type"
1051
+ echo ""
1052
+
1053
+ # 3b. Auto-snapshot before update (skip in dry-run)
1054
+ if [ "$DRY_RUN" != true ] && [ -f "$manifest" ]; then
1055
+ echo "Creating pre-update snapshot..."
1056
+ do_snapshot 2>/dev/null
1057
+ PRE_UPDATE_SNAP=$(ls -t "$TARGET/.cortexhawk-snapshots"/*.json 2>/dev/null | head -1)
1058
+ [ -n "$PRE_UPDATE_SNAP" ] && echo " Saved: $(basename "$PRE_UPDATE_SNAP")"
1059
+ echo ""
1060
+ fi
1061
+
1062
+ # 4. Pull source (skip in dry-run — compare against current source)
1063
+ if [ "$DRY_RUN" != true ]; then
1064
+ if [ "$source_type" = "git" ]; then
1065
+ echo "Updating CortexHawk source via git pull..."
1066
+ update_source_git
1067
+ else
1068
+ echo "Updating CortexHawk source via download..."
1069
+ update_source_release
1070
+ fi
1071
+ echo " Source updated successfully."
1072
+ fi
1073
+
1074
+ # 5. Compare versions
1075
+ local new_version
1076
+ new_version=$(get_version)
1077
+ echo " New version: $new_version"
1078
+ echo ""
1079
+
1080
+ if [ "$current_version" = "$new_version" ] && [ "$FORCE_MODE" != true ]; then
1081
+ # Same version — check if files actually changed (checksum comparison)
1082
+ local files_changed=0
1083
+ if [ -f "$manifest" ]; then
1084
+ while IFS= read -r line; do
1085
+ local fpath fhash
1086
+ fpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
1087
+ fhash=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
1088
+ [ -z "$fpath" ] || [ -z "$fhash" ] && continue
1089
+ local source_file="$SCRIPT_DIR/$fpath"
1090
+ [ -f "$source_file" ] || continue
1091
+ local source_hash
1092
+ source_hash=$(compute_checksum "$source_file")
1093
+ if [ "$fhash" != "$source_hash" ]; then
1094
+ files_changed=$((files_changed + 1))
1095
+ fi
1096
+ done < <(grep '"sha256:' "$manifest")
1097
+ fi
1098
+
1099
+ if [ "$files_changed" -eq 0 ] && [ "$DRY_RUN" != true ]; then
1100
+ # Still apply install improvements even if no component files changed
1101
+ local target_dir_name=".${TARGET_CLI:-claude}"
1102
+ update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
1103
+ [ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
1104
+ if [ ! -f "$TARGET/git-workflow.conf" ]; then
1105
+ GIT_BRANCHING="direct-main"
1106
+ GIT_COMMIT_CONVENTION="conventional"
1107
+ GIT_PR_PREFERENCE="on-demand"
1108
+ GIT_AUTO_PUSH="after-commit"
1109
+ GIT_WORK_BRANCH=""
1110
+ source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$(dirname "$TARGET")" "$TARGET"
1111
+ fi
1112
+ echo "Already up to date ($new_version). No component files changed."
1113
+ cleanup_update
1114
+ exit 0
1115
+ elif [ "$files_changed" -gt 0 ]; then
1116
+ echo " Same version ($new_version) but $files_changed file(s) changed in source."
1117
+ fi
1118
+ fi
1119
+
1120
+ if [ "$DRY_RUN" = true ]; then
1121
+ echo " Comparing source $new_version vs installed $current_version"
1122
+ elif [ "$current_version" = "$new_version" ]; then
1123
+ echo " Syncing changed files ($new_version)..."
1124
+ else
1125
+ echo " Updating $current_version -> $new_version"
1126
+ fi
1127
+ echo ""
1128
+
1129
+ # Set up profile file for skill filtering
1130
+ if [ -n "$update_profile" ] && [ "$update_profile" != "all" ]; then
1131
+ PROFILE_FILE="$SCRIPT_DIR/profiles/${update_profile}.json"
1132
+ if [ ! -f "$PROFILE_FILE" ]; then
1133
+ echo " Warning: profile '$update_profile' not found in source — installing all skills"
1134
+ update_profile="all"
1135
+ PROFILE_FILE=""
1136
+ fi
1137
+ fi
1138
+
1139
+ # 6. Reset counters and sync components
1140
+ SYNC_ADDED=0
1141
+ SYNC_UPDATED=0
1142
+ SYNC_UNCHANGED=0
1143
+ SYNC_SKIPPED=0
1144
+ SYNC_CONFLICTS=0
1145
+
1146
+ sync_component "agents"
1147
+ sync_component "commands"
1148
+ sync_skills_update "$update_profile"
1149
+ sync_component "hooks"
1150
+ sync_component "modes"
1151
+ sync_component "mcp"
1152
+
1153
+ # 6b. Sync agent personas from project root
1154
+ local project_root
1155
+ project_root="$(dirname "$TARGET")"
1156
+ if [ -d "$project_root/.cortexhawk-agents" ] && [ "$DRY_RUN" != true ]; then
1157
+ cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
1158
+ local pc
1159
+ pc=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
1160
+ [ "$pc" -gt 0 ] && echo " Synced $pc agent persona(s) from .cortexhawk-agents/"
1161
+ fi
1162
+
1163
+ # 7. Detect removed upstream files
1164
+ if [ -f "$manifest" ]; then
1165
+ while IFS= read -r line; do
1166
+ local file_relpath
1167
+ file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/')
1168
+ [ -z "$file_relpath" ] && continue
1169
+ if [ ! -f "$SCRIPT_DIR/$file_relpath" ] && [ -f "$TARGET/$file_relpath" ]; then
1170
+ echo " Warning: $file_relpath was removed upstream (kept locally)"
1171
+ fi
1172
+ done < <(grep '"sha256:' "$manifest")
1173
+ fi
1174
+
1175
+ if [ "$DRY_RUN" != true ]; then
1176
+ # Make hooks executable
1177
+ chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
1178
+
1179
+ # 7b. Regenerate settings.json hooks section from compose.yml
1180
+ if [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
1181
+ local hooks_json
1182
+ hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
1183
+ if [ -n "$hooks_json" ]; then
1184
+ python3 << PYEOF
1185
+ import json, sys
1186
+
1187
+ # Read current settings.json to preserve permissions
1188
+ current = {}
1189
+ try:
1190
+ with open('$TARGET/settings.json') as f:
1191
+ current = json.load(f)
1192
+ except:
1193
+ pass
1194
+
1195
+ # Merge: keep existing permissions, replace hooks
1196
+ hooks = json.loads('''$hooks_json''')
1197
+ current['hooks'] = hooks
1198
+
1199
+ with open('$TARGET/settings.json', 'w') as f:
1200
+ json.dump(current, f, indent=2)
1201
+ f.write('\n')
1202
+ PYEOF
1203
+ echo " Regenerated settings.json hooks from compose.yml"
1204
+ fi
1205
+ fi
1206
+
1207
+ # 8. Write new manifest
1208
+ write_manifest "$TARGET" "$update_profile" "$TARGET_CLI" true
1209
+
1210
+ # 9. Run audit
1211
+ run_audit "$(dirname "$TARGET")"
1212
+
1213
+ # 10. Apply install improvements (gitignore, git-workflow defaults)
1214
+ local target_dir_name=".${TARGET_CLI:-claude}"
1215
+ update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
1216
+ [ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
1217
+
1218
+ if [ ! -f "$TARGET/git-workflow.conf" ]; then
1219
+ GIT_BRANCHING="direct-main"
1220
+ GIT_COMMIT_CONVENTION="conventional"
1221
+ GIT_PR_PREFERENCE="on-demand"
1222
+ GIT_AUTO_PUSH="after-commit"
1223
+ GIT_WORK_BRANCH=""
1224
+ source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$(dirname "$TARGET")" "$TARGET"
1225
+ fi
1226
+ fi
1227
+
1228
+ # 10. Print summary
1229
+ echo ""
1230
+ if [ "$DRY_RUN" = true ]; then
1231
+ echo "Dry run summary:"
1232
+ echo " Would add: $SYNC_ADDED"
1233
+ echo " Would update: $SYNC_UPDATED"
1234
+ echo " Unchanged: $SYNC_UNCHANGED"
1235
+ echo " Would skip: $SYNC_SKIPPED"
1236
+ echo " Conflicts: $SYNC_CONFLICTS"
1237
+ echo ""
1238
+ echo "No files were modified (dry run)."
1239
+ else
1240
+ echo "Update complete: $current_version -> $new_version"
1241
+ echo " Added: $SYNC_ADDED"
1242
+ echo " Updated: $SYNC_UPDATED"
1243
+ echo " Unchanged: $SYNC_UNCHANGED"
1244
+ echo " Skipped: $SYNC_SKIPPED"
1245
+ echo " Conflicts: $SYNC_CONFLICTS"
1246
+ if [ -n "${PRE_UPDATE_SNAP:-}" ]; then
1247
+ echo " Rollback: install.sh --restore $PRE_UPDATE_SNAP"
1248
+ fi
1249
+ echo ""
1250
+ echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
1251
+ fi
1252
+
1253
+ cleanup_update
1254
+ }
1255
+
1256
+ # --- rotate_snapshots() ---
1257
+ # Keeps only the N most recent snapshots, deletes the rest
1258
+ rotate_snapshots() {
1259
+ local snap_dir="$1"
1260
+ local snaps
1261
+ snaps=$(find "$snap_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
1262
+ [ -z "$snaps" ] && return 0
1263
+ local count
1264
+ count=$(echo "$snaps" | wc -l | tr -d ' ')
1265
+ if [ "$count" -gt "$MAX_SNAPSHOTS" ]; then
1266
+ local to_delete=$((count - MAX_SNAPSHOTS))
1267
+ ls -1t "$snap_dir"/*.json | tail -n "$to_delete" | while read -r old_snap; do
1268
+ rm -f "$old_snap"
1269
+ done
1270
+ echo " Rotated: removed $to_delete old snapshot(s), keeping $MAX_SNAPSHOTS"
1271
+ fi
1272
+ }
1273
+
1274
+ # --- do_snapshot() ---
1275
+ do_snapshot() {
1276
+ if [ "$GLOBAL" = true ]; then
1277
+ TARGET="$HOME/.claude"
1278
+ else
1279
+ TARGET="$(pwd)/.claude"
1280
+ fi
1281
+
1282
+ local manifest="$TARGET/.cortexhawk-manifest"
1283
+ if [ ! -f "$manifest" ]; then
1284
+ echo "Error: no CortexHawk manifest found at $manifest"
1285
+ echo "Run install.sh first to create an installation"
1286
+ exit 1
1287
+ fi
1288
+
1289
+ # Read manifest metadata
1290
+ local version
1291
+ version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1292
+ local profile
1293
+ profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1294
+ local source_type
1295
+ source_type=$(grep '"source"' "$manifest" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1296
+ local source_url
1297
+ source_url=$(grep '"source_url"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1298
+ local source_path
1299
+ source_path=$(grep '"source_path"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
1300
+
1301
+ # Read settings.json
1302
+ local settings_json="null"
1303
+ if [ -f "$TARGET/settings.json" ]; then
1304
+ settings_json=$(cat "$TARGET/settings.json")
1305
+ fi
1306
+
1307
+ # Read git-workflow.conf
1308
+ local git_branching="" git_commit="" git_pr="" git_push=""
1309
+ if [ -f "$TARGET/git-workflow.conf" ]; then
1310
+ git_branching=$(grep '^BRANCHING=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1311
+ git_commit=$(grep '^COMMIT_CONVENTION=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1312
+ git_pr=$(grep '^PR_PREFERENCE=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1313
+ git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1314
+ fi
1315
+
1316
+ # Read custom profile if applicable
1317
+ local profile_def="null"
1318
+ local custom_profile
1319
+ custom_profile=$(ls /tmp/cortexhawk-custom-*.json 2>/dev/null | head -1)
1320
+ if [ -n "$custom_profile" ] && [ -f "$custom_profile" ]; then
1321
+ profile_def=$(cat "$custom_profile")
1322
+ fi
1323
+
1324
+ # Build files checksums from manifest
1325
+ local files_json
1326
+ files_json=$(sed -n '/"files"/,/^ }/p' "$manifest" | sed '1d;$d')
1327
+
1328
+ # Collect file contents (base64 encoded for binary safety)
1329
+ local git_workflow_content="" claude_md_content=""
1330
+ if [ -f "$TARGET/git-workflow.conf" ]; then
1331
+ git_workflow_content=$(base64 < "$TARGET/git-workflow.conf" | tr -d '\n')
1332
+ fi
1333
+ if [ -f "$TARGET/../CLAUDE.md" ]; then
1334
+ claude_md_content=$(base64 < "$TARGET/../CLAUDE.md" | tr -d '\n')
1335
+ fi
1336
+
1337
+ # Generate snapshot
1338
+ local now
1339
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1340
+ local snap_name
1341
+ snap_name=$(date -u +"%Y-%m-%d-%H%M%S")
1342
+ local snap_dir="$TARGET/.cortexhawk-snapshots"
1343
+ local snap_file="$snap_dir/${snap_name}.json"
1344
+
1345
+ mkdir -p "$snap_dir"
1346
+
1347
+ # Write snapshot JSON
1348
+ printf '{\n' > "$snap_file"
1349
+ printf ' "snapshot_version": "2",\n' >> "$snap_file"
1350
+ printf ' "snapshot_date": "%s",\n' "$now" >> "$snap_file"
1351
+ printf ' "cortexhawk_version": "%s",\n' "$version" >> "$snap_file"
1352
+ printf ' "target": "claude",\n' >> "$snap_file"
1353
+ printf ' "profile": "%s",\n' "$profile" >> "$snap_file"
1354
+ printf ' "profile_definition": %s,\n' "$profile_def" >> "$snap_file"
1355
+ printf ' "source": "%s",\n' "$source_type" >> "$snap_file"
1356
+ printf ' "source_url": "%s",\n' "$source_url" >> "$snap_file"
1357
+ printf ' "source_path": "%s",\n' "$source_path" >> "$snap_file"
1358
+ printf ' "settings": %s,\n' "$settings_json" >> "$snap_file"
1359
+ printf ' "git_workflow": {\n' >> "$snap_file"
1360
+ printf ' "BRANCHING": "%s",\n' "$git_branching" >> "$snap_file"
1361
+ printf ' "COMMIT_CONVENTION": "%s",\n' "$git_commit" >> "$snap_file"
1362
+ printf ' "PR_PREFERENCE": "%s",\n' "$git_pr" >> "$snap_file"
1363
+ printf ' "AUTO_PUSH": "%s"\n' "$git_push" >> "$snap_file"
1364
+ printf ' },\n' >> "$snap_file"
1365
+ printf ' "files": {\n' >> "$snap_file"
1366
+ printf '%s\n' "$files_json" >> "$snap_file"
1367
+ printf ' },\n' >> "$snap_file"
1368
+ printf ' "file_contents": {\n' >> "$snap_file"
1369
+ [ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
1370
+ [ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
1371
+ # Remove trailing comma from last entry
1372
+ sed -i '$ s/,$//' "$snap_file"
1373
+ printf ' }\n' >> "$snap_file"
1374
+ printf '}\n' >> "$snap_file"
1375
+
1376
+ echo "CortexHawk Snapshot"
1377
+ echo "====================="
1378
+ echo " Version: $version"
1379
+ echo " Profile: $profile"
1380
+ echo " Target: $TARGET"
1381
+ echo " Saved to: $snap_file"
1382
+
1383
+ # Create portable archive if requested
1384
+ if [ "$PORTABLE_MODE" = true ]; then
1385
+ local archive_name="${snap_name}.tar.gz"
1386
+ local archive_path="$snap_dir/$archive_name"
1387
+ local tmp_dir
1388
+ tmp_dir=$(mktemp -d)
1389
+
1390
+ # Copy snapshot and files to temp structure
1391
+ cp "$snap_file" "$tmp_dir/snapshot.json"
1392
+ mkdir -p "$tmp_dir/files"
1393
+ cp -r "$TARGET/agents" "$tmp_dir/files/" 2>/dev/null || true
1394
+ cp -r "$TARGET/commands" "$tmp_dir/files/" 2>/dev/null || true
1395
+ cp -r "$TARGET/skills" "$tmp_dir/files/" 2>/dev/null || true
1396
+ cp -r "$TARGET/hooks" "$tmp_dir/files/" 2>/dev/null || true
1397
+ cp -r "$TARGET/modes" "$tmp_dir/files/" 2>/dev/null || true
1398
+ cp -r "$TARGET/mcp" "$tmp_dir/files/" 2>/dev/null || true
1399
+ cp "$TARGET/settings.json" "$tmp_dir/files/" 2>/dev/null || true
1400
+ cp "$TARGET/git-workflow.conf" "$tmp_dir/files/" 2>/dev/null || true
1401
+
1402
+ # Create archive
1403
+ tar -czf "$archive_path" -C "$tmp_dir" .
1404
+ rm -rf "$tmp_dir"
1405
+
1406
+ echo " Archive: $archive_path"
1407
+ echo ""
1408
+ echo "Restore with: install.sh --restore $archive_path"
1409
+ else
1410
+ rotate_snapshots "$snap_dir"
1411
+ echo ""
1412
+ echo "Restore with: install.sh --restore $snap_file"
1413
+ fi
1414
+ }
1415
+
1416
+ # --- do_snapshots_list() ---
1417
+ do_snapshots_list() {
1418
+ if [ "$GLOBAL" = true ]; then
1419
+ TARGET="$HOME/.claude"
1420
+ else
1421
+ TARGET="$(pwd)/.claude"
1422
+ fi
1423
+
1424
+ local snap_dir="$TARGET/.cortexhawk-snapshots"
1425
+ if [ ! -d "$snap_dir" ] || [ -z "$(ls "$snap_dir"/*.json 2>/dev/null)" ]; then
1426
+ echo "No snapshots found in $snap_dir"
1427
+ echo "Create one with: install.sh --snapshot"
1428
+ exit 0
1429
+ fi
1430
+
1431
+ echo "CortexHawk Snapshots"
1432
+ echo "====================="
1433
+ printf " %-25s %-10s %-12s %s\n" "DATE" "VERSION" "PROFILE" "FILE"
1434
+ printf " %-25s %-10s %-12s %s\n" "----" "-------" "-------" "----"
1435
+
1436
+ for snap_file in $(ls -t "$snap_dir"/*.json 2>/dev/null); do
1437
+ local date version profile
1438
+ date=$(grep '"snapshot_date"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1439
+ version=$(grep '"cortexhawk_version"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1440
+ profile=$(grep '"profile"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1441
+ printf " %-25s %-10s %-12s %s\n" "$date" "$version" "$profile" "$(basename "$snap_file")"
1442
+ done
1443
+
1444
+ echo ""
1445
+ echo "Restore with: install.sh --restore <path-to-snapshot>"
1446
+ }
1447
+
1448
+ # --- do_diff() ---
1449
+ do_diff() {
1450
+ if [ "$GLOBAL" = true ]; then
1451
+ TARGET="$HOME/.claude"
1452
+ else
1453
+ TARGET="$(pwd)/.claude"
1454
+ fi
1455
+
1456
+ # Resolve reference file: snapshot or manifest
1457
+ local ref_file="$DIFF_FILE"
1458
+ local ref_label="manifest"
1459
+ if [ -z "$ref_file" ]; then
1460
+ ref_file="$TARGET/.cortexhawk-manifest"
1461
+ ref_label="manifest"
1462
+ else
1463
+ ref_label="snapshot $(basename "$ref_file")"
1464
+ fi
1465
+
1466
+ if [ ! -f "$ref_file" ]; then
1467
+ echo "Error: reference file not found: $ref_file"
1468
+ [ -z "$DIFF_FILE" ] && echo "Run install.sh first to create an installation"
1469
+ exit 1
1470
+ fi
1471
+
1472
+ echo "CortexHawk Diff"
1473
+ echo "================="
1474
+ echo " Reference: $ref_label"
1475
+ echo " Target: $TARGET"
1476
+ echo ""
1477
+
1478
+ local modified=0 missing=0 unchanged=0
1479
+ local tracked_files=""
1480
+
1481
+ # Compare files listed in reference
1482
+ while IFS= read -r line; do
1483
+ local file_relpath
1484
+ file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
1485
+ local expected
1486
+ expected=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
1487
+ [ -z "$file_relpath" ] || [ -z "$expected" ] && continue
1488
+
1489
+ tracked_files="$tracked_files|$file_relpath|"
1490
+ local target_file="$TARGET/$file_relpath"
1491
+
1492
+ if [ ! -f "$target_file" ]; then
1493
+ echo " MISSING $file_relpath"
1494
+ missing=$((missing + 1))
1495
+ else
1496
+ local actual
1497
+ actual=$(compute_checksum "$target_file")
1498
+ if [ "$actual" = "$expected" ]; then
1499
+ unchanged=$((unchanged + 1))
1500
+ else
1501
+ echo " MODIFIED $file_relpath"
1502
+ modified=$((modified + 1))
1503
+ fi
1504
+ fi
1505
+ done < <(grep '"sha256:' "$ref_file")
1506
+
1507
+ # Detect NEW files not in reference
1508
+ local new_count=0
1509
+ if [ -d "$TARGET" ]; then
1510
+ while IFS= read -r file; do
1511
+ local relpath="${file#$TARGET/}"
1512
+ case "$relpath" in
1513
+ .cortexhawk-manifest|.cortexhawk-snapshots/*|settings.json|git-workflow.conf) continue ;;
1514
+ esac
1515
+ if [[ "$tracked_files" != *"|$relpath|"* ]]; then
1516
+ echo " NEW $relpath"
1517
+ new_count=$((new_count + 1))
1518
+ fi
1519
+ done < <(find "$TARGET" -type f | sort)
1520
+ fi
1521
+
1522
+ echo ""
1523
+ echo "Summary:"
1524
+ echo " Unchanged: $unchanged"
1525
+ echo " Modified: $modified"
1526
+ echo " Missing: $missing"
1527
+ echo " New: $new_count"
1528
+ }
1529
+
1530
+ # --- do_diff_semantic() ---
1531
+ # Compares two snapshot files and displays semantic differences
1532
+ do_diff_semantic() {
1533
+ local file_a="$1"
1534
+ local file_b="$2"
1535
+
1536
+ if [ ! -f "$file_a" ]; then
1537
+ echo "Error: snapshot not found: $file_a"
1538
+ exit 1
1539
+ fi
1540
+ if [ ! -f "$file_b" ]; then
1541
+ echo "Error: snapshot not found: $file_b"
1542
+ exit 1
1543
+ fi
1544
+
1545
+ echo "CortexHawk Semantic Diff"
1546
+ echo "========================="
1547
+ echo " A: $(basename "$file_a")"
1548
+ echo " B: $(basename "$file_b")"
1549
+ echo ""
1550
+
1551
+ local changes=0
1552
+
1553
+ # Helper: extract JSON string value by key (anchored to line start)
1554
+ _sdiff_val() { grep "^[[:space:]]*\"$2\"" "$1" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/'; }
1555
+
1556
+ # 1. Metadata comparison
1557
+ local section_changes=0
1558
+ for key in cortexhawk_version profile target source; do
1559
+ local val_a val_b
1560
+ val_a=$(_sdiff_val "$file_a" "$key")
1561
+ val_b=$(_sdiff_val "$file_b" "$key")
1562
+ if [ "$val_a" != "$val_b" ]; then
1563
+ [ "$section_changes" -eq 0 ] && echo "Metadata:"
1564
+ echo " $key: $val_a → $val_b"
1565
+ section_changes=$((section_changes + 1))
1566
+ changes=$((changes + 1))
1567
+ fi
1568
+ done
1569
+ [ "$section_changes" -gt 0 ] && echo ""
1570
+
1571
+ # 2. Git workflow comparison
1572
+ section_changes=0
1573
+ for key in BRANCHING COMMIT_CONVENTION PR_PREFERENCE AUTO_PUSH; do
1574
+ local val_a val_b
1575
+ val_a=$(_sdiff_val "$file_a" "$key")
1576
+ val_b=$(_sdiff_val "$file_b" "$key")
1577
+ if [ "$val_a" != "$val_b" ]; then
1578
+ [ "$section_changes" -eq 0 ] && echo "Git Workflow:"
1579
+ echo " $key: $val_a → $val_b"
1580
+ section_changes=$((section_changes + 1))
1581
+ changes=$((changes + 1))
1582
+ fi
1583
+ done
1584
+ [ "$section_changes" -gt 0 ] && echo ""
1585
+
1586
+ # 3. Skills comparison (extract skill paths from files keys)
1587
+ local skills_a skills_b
1588
+ skills_a=$(grep '"skills/' "$file_a" | sed 's/.*"\(skills\/[^"]*\)".*/\1/' | sort)
1589
+ skills_b=$(grep '"skills/' "$file_b" | sed 's/.*"\(skills\/[^"]*\)".*/\1/' | sort)
1590
+
1591
+ local added removed
1592
+ added=$(comm -13 <(echo "$skills_a") <(echo "$skills_b"))
1593
+ removed=$(comm -23 <(echo "$skills_a") <(echo "$skills_b"))
1594
+
1595
+ if [ -n "$added" ] || [ -n "$removed" ]; then
1596
+ echo "Skills:"
1597
+ while IFS= read -r skill; do
1598
+ [ -n "$skill" ] && echo " + $skill" && changes=$((changes + 1))
1599
+ done <<< "$added"
1600
+ while IFS= read -r skill; do
1601
+ [ -n "$skill" ] && echo " - $skill" && changes=$((changes + 1))
1602
+ done <<< "$removed"
1603
+ echo ""
1604
+ fi
1605
+
1606
+ # 4. Files comparison (non-skill files: agents, hooks, commands, modes)
1607
+ local files_a files_b
1608
+ files_a=$(grep '"sha256:' "$file_a" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/' | grep -v '^skills/' | sort)
1609
+ files_b=$(grep '"sha256:' "$file_b" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/' | grep -v '^skills/' | sort)
1610
+
1611
+ local files_added files_removed
1612
+ files_added=$(comm -13 <(echo "$files_a") <(echo "$files_b"))
1613
+ files_removed=$(comm -23 <(echo "$files_a") <(echo "$files_b"))
1614
+
1615
+ # Check for modified files (same path, different checksum)
1616
+ local files_modified=""
1617
+ while IFS= read -r fpath; do
1618
+ [ -z "$fpath" ] && continue
1619
+ local cs_a cs_b
1620
+ cs_a=$(grep "\"$fpath\"" "$file_a" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
1621
+ cs_b=$(grep "\"$fpath\"" "$file_b" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
1622
+ if [ -n "$cs_a" ] && [ -n "$cs_b" ] && [ "$cs_a" != "$cs_b" ]; then
1623
+ files_modified="${files_modified}${fpath}\n"
1624
+ fi
1625
+ done < <(comm -12 <(echo "$files_a") <(echo "$files_b"))
1626
+
1627
+ if [ -n "$files_added" ] || [ -n "$files_removed" ] || [ -n "$files_modified" ]; then
1628
+ echo "Files:"
1629
+ while IFS= read -r f; do
1630
+ [ -n "$f" ] && echo " + $f" && changes=$((changes + 1))
1631
+ done <<< "$files_added"
1632
+ while IFS= read -r f; do
1633
+ [ -n "$f" ] && echo " - $f" && changes=$((changes + 1))
1634
+ done <<< "$files_removed"
1635
+ # Use process substitution to avoid subshell (pipe would lose changes counter)
1636
+ while IFS= read -r f; do
1637
+ [ -n "$f" ] && echo " ~ $f" && changes=$((changes + 1))
1638
+ done < <(printf '%b' "$files_modified")
1639
+ echo ""
1640
+ fi
1641
+
1642
+ if [ "$changes" -eq 0 ]; then
1643
+ echo "No differences found — snapshots are identical."
1644
+ else
1645
+ echo "Total: $changes difference(s)"
1646
+ fi
1647
+
1648
+ unset -f _sdiff_val
1649
+ }
1650
+
1651
+ # --- do_export_team() ---
1652
+ do_export_team() {
1653
+ if [ "$GLOBAL" = true ]; then
1654
+ TARGET="$HOME/.claude"
1655
+ else
1656
+ TARGET="$(pwd)/.claude"
1657
+ fi
1658
+
1659
+ # Resolve reference file
1660
+ local ref_file="$EXPORT_TEAM_FILE"
1661
+ if [ -z "$ref_file" ]; then
1662
+ ref_file="$TARGET/.cortexhawk-manifest"
1663
+ fi
1664
+ if [ ! -f "$ref_file" ]; then
1665
+ echo "Error: reference file not found: $ref_file"
1666
+ exit 1
1667
+ fi
1668
+
1669
+ local profile
1670
+ profile=$(grep '"profile"' "$ref_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1671
+
1672
+ # Extract skills from file paths
1673
+ local skills=""
1674
+ while IFS= read -r line; do
1675
+ local path
1676
+ path=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/')
1677
+ case "$path" in
1678
+ skills/*/SKILL.md)
1679
+ local skill
1680
+ skill=$(echo "$path" | sed 's|skills/||;s|/SKILL.md||')
1681
+ skills="${skills:+$skills\n} - $skill"
1682
+ ;;
1683
+ esac
1684
+ done < <(grep '"sha256:' "$ref_file")
1685
+
1686
+ # Extract hooks (only .sh files directly in hooks/)
1687
+ local hooks=""
1688
+ while IFS= read -r line; do
1689
+ local path
1690
+ path=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/')
1691
+ case "$path" in
1692
+ hooks/*.sh)
1693
+ local hook
1694
+ hook=$(basename "$path" .sh)
1695
+ hooks="${hooks:+$hooks\n} - $hook"
1696
+ ;;
1697
+ esac
1698
+ done < <(grep '"sha256:' "$ref_file")
1699
+
1700
+ # Extract modes (only .md files directly in modes/)
1701
+ local modes=""
1702
+ while IFS= read -r line; do
1703
+ local path
1704
+ path=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:.*/\1/')
1705
+ case "$path" in
1706
+ modes/*.md)
1707
+ local mode
1708
+ mode=$(basename "$path" .md)
1709
+ modes="${modes:+$modes\n} - $mode"
1710
+ ;;
1711
+ esac
1712
+ done < <(grep '"sha256:' "$ref_file")
1713
+
1714
+ # Read git workflow
1715
+ local gb="" gc="" gp="" ga=""
1716
+ if grep -q '"BRANCHING"' "$ref_file" 2>/dev/null; then
1717
+ gb=$(grep '"BRANCHING"' "$ref_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1718
+ gc=$(grep '"COMMIT_CONVENTION"' "$ref_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1719
+ gp=$(grep '"PR_PREFERENCE"' "$ref_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1720
+ ga=$(grep '"AUTO_PUSH"' "$ref_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1721
+ elif [ -f "$TARGET/git-workflow.conf" ]; then
1722
+ gb=$(grep '^BRANCHING=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1723
+ gc=$(grep '^COMMIT_CONVENTION=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1724
+ gp=$(grep '^PR_PREFERENCE=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1725
+ ga=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1726
+ fi
1727
+
1728
+ # Generate YAML
1729
+ local output=".cortexhawk-team.yml"
1730
+ {
1731
+ echo "# .cortexhawk-team.yml"
1732
+ echo "# Generated from $(basename "$ref_file") by CortexHawk"
1733
+ echo "version: \"1\""
1734
+ echo "profile: ${profile:-all}"
1735
+ if [ -n "$skills" ]; then
1736
+ echo "skills:"
1737
+ printf '%b\n' "$skills"
1738
+ fi
1739
+ if [ -n "$hooks" ]; then
1740
+ echo "hooks:"
1741
+ printf '%b\n' "$hooks"
1742
+ fi
1743
+ if [ -n "$modes" ]; then
1744
+ echo "modes:"
1745
+ printf '%b\n' "$modes"
1746
+ fi
1747
+ if [ -n "$gb" ]; then
1748
+ echo "git_workflow:"
1749
+ echo " branching: $gb"
1750
+ echo " commit_convention: $gc"
1751
+ echo " pr_preference: $gp"
1752
+ echo " auto_push: $ga"
1753
+ fi
1754
+ } > "$output"
1755
+
1756
+ echo "CortexHawk Export Team"
1757
+ echo "========================"
1758
+ echo " Source: $(basename "$ref_file")"
1759
+ echo " Profile: ${profile:-all}"
1760
+ echo " Output: $output"
1761
+ echo ""
1762
+ echo "Share this file with your team. Install with: install.sh --team"
1763
+ }
1764
+
1765
+ # --- do_restore() ---
1766
+ do_restore() {
1767
+ local snap_file="$1"
1768
+ local archive_tmp=""
1769
+ local portable_files=""
1770
+
1771
+ if [ -z "$snap_file" ] || [ ! -f "$snap_file" ]; then
1772
+ echo "Error: snapshot file not found: $snap_file"
1773
+ exit 1
1774
+ fi
1775
+
1776
+ # Handle portable archive (.tar.gz)
1777
+ if [[ "$snap_file" == *.tar.gz ]]; then
1778
+ archive_tmp=$(mktemp -d)
1779
+ tar -xzf "$snap_file" -C "$archive_tmp"
1780
+ snap_file="$archive_tmp/snapshot.json"
1781
+ portable_files="$archive_tmp/files"
1782
+ if [ ! -f "$snap_file" ]; then
1783
+ echo "Error: invalid archive — snapshot.json not found"
1784
+ rm -rf "$archive_tmp"
1785
+ exit 1
1786
+ fi
1787
+ echo "Extracting portable archive..."
1788
+ fi
1789
+
1790
+ # Extract metadata from snapshot
1791
+ local snap_version
1792
+ snap_version=$(grep '"cortexhawk_version"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1793
+ local snap_profile
1794
+ snap_profile=$(grep '"profile"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1795
+ local snap_source
1796
+ snap_source=$(grep '"source"' "$snap_file" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
1797
+ local snap_source_url
1798
+ snap_source_url=$(grep '"source_url"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1799
+ local snap_source_path
1800
+ snap_source_path=$(grep '"source_path"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1801
+
1802
+ echo "CortexHawk Restore"
1803
+ echo "====================="
1804
+ echo " Snapshot version: $snap_version"
1805
+ echo " Profile: $snap_profile"
1806
+ echo " Source: $snap_source"
1807
+ echo ""
1808
+
1809
+ # Determine CortexHawk source
1810
+ local restore_source="$SCRIPT_DIR"
1811
+ if [ "$snap_source" = "git" ] && [ -n "$snap_source_path" ] && [ -d "$snap_source_path" ]; then
1812
+ restore_source="$snap_source_path"
1813
+ fi
1814
+ if [ ! -d "$restore_source/agents" ]; then
1815
+ echo "Error: CortexHawk source not found at $restore_source"
1816
+ echo "Ensure the CortexHawk repo is available or set CORTEXHAWK_REPO"
1817
+ exit 1
1818
+ fi
1819
+
1820
+ # Warn if source version differs from snapshot
1821
+ local current_version
1822
+ current_version=$(get_version)
1823
+ if [ "$snap_version" != "$current_version" ]; then
1824
+ echo " Warning: snapshot is v$snap_version but source is v$current_version"
1825
+ echo " Some file checksums may not match"
1826
+ echo ""
1827
+ fi
1828
+
1829
+ # Set profile for reinstall
1830
+ if [ -n "$snap_profile" ] && [ "$snap_profile" != "all" ]; then
1831
+ PROFILE="$snap_profile"
1832
+ PROFILE_FILE="$restore_source/profiles/${snap_profile}.json"
1833
+ if [ ! -f "$PROFILE_FILE" ]; then
1834
+ echo " Warning: profile '$snap_profile' not found — installing all skills"
1835
+ PROFILE=""
1836
+ PROFILE_FILE=""
1837
+ fi
1838
+ fi
1839
+
1840
+ # Determine target
1841
+ if [ "$GLOBAL" = true ]; then
1842
+ TARGET="$HOME/.claude"
1843
+ else
1844
+ TARGET="$(pwd)/.claude"
1845
+ fi
1846
+
1847
+ # Save the original SCRIPT_DIR, use snapshot source
1848
+ local orig_script_dir="$SCRIPT_DIR"
1849
+ SCRIPT_DIR="$restore_source"
1850
+
1851
+ # Reinstall using the standard flow
1852
+ echo "Reinstalling CortexHawk components..."
1853
+ mkdir -p "$TARGET"/{agents,commands,skills,hooks,modes,mcp}
1854
+
1855
+ # Use portable archive files if available, otherwise use source repo
1856
+ if [ -n "$portable_files" ] && [ -d "$portable_files" ]; then
1857
+ echo " Using files from portable archive..."
1858
+ cp -r "$portable_files/agents/"* "$TARGET/agents/" 2>/dev/null || true
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
1864
+ else
1865
+ cp -r "$SCRIPT_DIR/agents/"* "$TARGET/agents/" 2>/dev/null || true
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
1871
+ fi
1872
+ chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
1873
+
1874
+ # Restore settings.json from snapshot
1875
+ if command -v python3 >/dev/null 2>&1; then
1876
+ python3 -c "
1877
+ import json, sys
1878
+ with open('$snap_file') as f:
1879
+ snap = json.load(f)
1880
+ settings = snap.get('settings')
1881
+ if settings is not None:
1882
+ with open('$TARGET/settings.json', 'w') as f:
1883
+ json.dump(settings, f, indent=2)
1884
+ f.write('\n')
1885
+ print(' Restored settings.json')
1886
+ "
1887
+ else
1888
+ echo " Warning: python3 not found — settings.json not restored from snapshot"
1889
+ fi
1890
+
1891
+ # Restore git-workflow.conf from snapshot
1892
+ local branching commit_conv pr_pref auto_push
1893
+ branching=$(grep '"BRANCHING"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1894
+ commit_conv=$(grep '"COMMIT_CONVENTION"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1895
+ pr_pref=$(grep '"PR_PREFERENCE"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1896
+ auto_push=$(grep '"AUTO_PUSH"' "$snap_file" | sed 's/.*: *"\([^"]*\)".*/\1/')
1897
+
1898
+ if [ -n "$branching" ] || [ -n "$commit_conv" ] || [ -n "$pr_pref" ] || [ -n "$auto_push" ]; then
1899
+ {
1900
+ echo "BRANCHING=$branching"
1901
+ echo "COMMIT_CONVENTION=$commit_conv"
1902
+ echo "PR_PREFERENCE=$pr_pref"
1903
+ echo "AUTO_PUSH=$auto_push"
1904
+ } > "$TARGET/git-workflow.conf"
1905
+ echo " Restored git-workflow.conf (from git_workflow keys)"
1906
+ fi
1907
+
1908
+ # Restore file_contents (snapshot v2) — overwrites git-workflow.conf if present
1909
+ if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
1910
+ python3 -c "
1911
+ import json, base64, sys, os
1912
+ with open('$snap_file') as f:
1913
+ snap = json.load(f)
1914
+ contents = snap.get('file_contents', {})
1915
+ for filename, b64data in contents.items():
1916
+ try:
1917
+ data = base64.b64decode(b64data).decode('utf-8')
1918
+ if filename == 'CLAUDE.md':
1919
+ target_path = os.path.dirname('$TARGET') + '/CLAUDE.md'
1920
+ else:
1921
+ target_path = '$TARGET/' + filename
1922
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
1923
+ with open(target_path, 'w') as f:
1924
+ f.write(data)
1925
+ print(f' Restored {filename} (from file_contents)')
1926
+ except Exception as e:
1927
+ print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
1928
+ "
1929
+ fi
1930
+
1931
+ # Write new manifest
1932
+ write_manifest "$TARGET" "$PROFILE" "claude" false
1933
+
1934
+ # Verify checksums against snapshot
1935
+ local verified=0 mismatched=0 missing=0
1936
+ while IFS= read -r line; do
1937
+ local file_relpath
1938
+ file_relpath=$(echo "$line" | sed 's/.*"\([^"]*\)": "sha256:\([^"]*\)".*/\1/')
1939
+ local expected_checksum
1940
+ expected_checksum=$(echo "$line" | sed 's/.*"sha256:\([^"]*\)".*/\1/')
1941
+ [ -z "$file_relpath" ] || [ -z "$expected_checksum" ] && continue
1942
+
1943
+ local target_file="$TARGET/$file_relpath"
1944
+ if [ ! -f "$target_file" ]; then
1945
+ missing=$((missing + 1))
1946
+ else
1947
+ local actual_checksum
1948
+ actual_checksum=$(compute_checksum "$target_file")
1949
+ if [ "$actual_checksum" = "$expected_checksum" ]; then
1950
+ verified=$((verified + 1))
1951
+ else
1952
+ mismatched=$((mismatched + 1))
1953
+ fi
1954
+ fi
1955
+ done < <(grep '"sha256:' "$snap_file")
1956
+
1957
+ SCRIPT_DIR="$orig_script_dir"
1958
+
1959
+ echo ""
1960
+ echo "Restore complete"
1961
+ echo " Verified: $verified files match snapshot checksums"
1962
+ [ "$mismatched" -gt 0 ] && echo " Mismatched: $mismatched files differ (source version may differ from snapshot)"
1963
+ [ "$missing" -gt 0 ] && echo " Missing: $missing files not found in source"
1964
+ echo ""
1965
+ echo " To activate: exit your CLI (ctrl+c) and relaunch in this directory."
1966
+
1967
+ # Cleanup temp dir from portable archive
1968
+ [ -n "$archive_tmp" ] && rm -rf "$archive_tmp"
1969
+ }
1970
+
1971
+ # --- do_doctor() ---
1972
+ # Diagnose installation health
1973
+ do_doctor() {
1974
+ if [ "$GLOBAL" = true ]; then
1975
+ TARGET="$HOME/.claude"
1976
+ else
1977
+ TARGET="$(pwd)/.claude"
1978
+ fi
1979
+
1980
+ local ok=0 warn=0 err=0
1981
+ _doc_ok() { echo " [OK] $1"; ok=$((ok+1)); }
1982
+ _doc_warn() { echo " [WARN] $1"; warn=$((warn+1)); }
1983
+ _doc_err() { echo " [ERR] $1"; err=$((err+1)); }
1984
+
1985
+ # Header
1986
+ local version="" profile="" target_cli_name=""
1987
+ if [ -f "$TARGET/.cortexhawk-manifest" ]; then
1988
+ version=$(grep -o '"version": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
1989
+ profile=$(grep -o '"profile": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
1990
+ target_cli_name=$(grep -o '"target": "[^"]*"' "$TARGET/.cortexhawk-manifest" | head -1 | cut -d'"' -f4)
1991
+ fi
1992
+ echo "CortexHawk Doctor"
1993
+ echo "==================="
1994
+ echo " Installation: $TARGET"
1995
+ echo " Version: ${version:-unknown}"
1996
+ echo " Profile: ${profile:-unknown}"
1997
+ echo " Target: ${target_cli_name:-claude}"
1998
+ echo ""
1999
+ echo "Checks:"
2000
+
2001
+ # 1. Manifest
2002
+ if [ -f "$TARGET/.cortexhawk-manifest" ]; then
2003
+ _doc_ok "Manifest present"
2004
+ else
2005
+ _doc_err "Manifest missing — run install.sh to create an installation"
2006
+ fi
2007
+
2008
+ # 2. settings.json valid JSON
2009
+ if [ -f "$TARGET/settings.json" ]; then
2010
+ if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$TARGET/settings.json" 2>/dev/null; then
2011
+ _doc_ok "settings.json valid JSON"
2012
+ else
2013
+ _doc_err "settings.json invalid JSON"
2014
+ fi
2015
+ else
2016
+ _doc_warn "settings.json not found"
2017
+ fi
2018
+
2019
+ # 3. Component counts (compare installed vs source)
2020
+ for comp in agents commands modes; do
2021
+ local installed=0 source_count=0
2022
+ installed=$(find "$TARGET/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
2023
+ source_count=$(find "$SCRIPT_DIR/$comp" -name "*.md" -type f 2>/dev/null | wc -l)
2024
+ if [ "$installed" -eq "$source_count" ] 2>/dev/null; then
2025
+ _doc_ok "$installed/$source_count $comp installed"
2026
+ elif [ "$installed" -gt 0 ] 2>/dev/null; then
2027
+ _doc_warn "$installed/$source_count $comp installed"
2028
+ else
2029
+ _doc_err "0/$source_count $comp installed"
2030
+ fi
2031
+ done
2032
+
2033
+ # 4. Skills (profile-dependent, just count what's there)
2034
+ local skills_installed=0 skills_source=0
2035
+ skills_installed=$(find "$TARGET/skills" -name "*.md" -type f 2>/dev/null | wc -l)
2036
+ skills_source=$(find "$SCRIPT_DIR/skills" -name "*.md" -type f 2>/dev/null | wc -l)
2037
+ if [ "$skills_installed" -gt 0 ] 2>/dev/null; then
2038
+ _doc_ok "$skills_installed/$skills_source skills installed (profile: ${profile:-all})"
2039
+ else
2040
+ _doc_err "No skills installed"
2041
+ fi
2042
+
2043
+ # 5. Hooks executable
2044
+ local hooks_ok=0 hooks_total=0
2045
+ for hook in "$TARGET/hooks/"*.sh; do
2046
+ [ -f "$hook" ] || continue
2047
+ hooks_total=$((hooks_total+1))
2048
+ if [ -x "$hook" ]; then
2049
+ hooks_ok=$((hooks_ok+1))
2050
+ else
2051
+ _doc_warn "Hook not executable: $(basename "$hook")"
2052
+ fi
2053
+ done
2054
+ if [ "$hooks_total" -gt 0 ]; then
2055
+ if [ "$hooks_ok" -eq "$hooks_total" ]; then
2056
+ _doc_ok "$hooks_ok/$hooks_total hooks executable"
2057
+ fi
2058
+ else
2059
+ _doc_warn "No hooks found"
2060
+ fi
2061
+
2062
+ # 6. compose.yml vs settings.json coherence
2063
+ if [ -f "$TARGET/../hooks/compose.yml" ] || [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
2064
+ _doc_ok "compose.yml present"
2065
+ fi
2066
+
2067
+ # 7. MCP configs
2068
+ if [ -d "$TARGET/mcp" ] && [ "$(find "$TARGET/mcp" -type f 2>/dev/null | wc -l)" -gt 0 ]; then
2069
+ _doc_ok "MCP configs present"
2070
+ elif [ -d "$TARGET/mcp" ]; then
2071
+ _doc_warn "MCP directory exists but empty"
2072
+ fi
2073
+
2074
+ # 8. docs/ workspace
2075
+ local project_root
2076
+ project_root="$(dirname "$TARGET")"
2077
+ if [ -d "$project_root/docs" ]; then
2078
+ _doc_ok "docs/ workspace exists"
2079
+ else
2080
+ _doc_warn "docs/ workspace missing"
2081
+ fi
2082
+
2083
+ # 9. Broken symlinks in docs/plans/
2084
+ local broken=0
2085
+ if [ -d "$project_root/docs/plans" ]; then
2086
+ while IFS= read -r link; do
2087
+ [ -z "$link" ] && continue
2088
+ _doc_warn "Broken symlink: $link"
2089
+ broken=$((broken+1))
2090
+ done < <(find "$project_root/docs/plans" -type l ! -exec test -e {} \; -print 2>/dev/null)
2091
+ [ "$broken" -eq 0 ] && _doc_ok "No broken symlinks in docs/plans/"
2092
+ fi
2093
+
2094
+ # 10. git-workflow.conf
2095
+ if [ -f "$TARGET/git-workflow.conf" ]; then
2096
+ _doc_ok "git-workflow.conf present"
2097
+ else
2098
+ _doc_warn "git-workflow.conf not found (run --init to configure)"
2099
+ fi
2100
+
2101
+ # 11. CLAUDE.md at project root
2102
+ if [ -f "$project_root/CLAUDE.md" ]; then
2103
+ _doc_ok "CLAUDE.md present at project root"
2104
+ else
2105
+ _doc_warn "CLAUDE.md not found at project root"
2106
+ fi
2107
+
2108
+ # 12. Version match source vs manifest
2109
+ if [ -n "$version" ]; then
2110
+ local source_version
2111
+ source_version=$(get_version)
2112
+ if [ "$version" = "$source_version" ]; then
2113
+ _doc_ok "Version match: source $source_version = manifest $version"
2114
+ else
2115
+ _doc_warn "Version mismatch: source $source_version != manifest $version (run --update)"
2116
+ fi
2117
+ fi
2118
+
2119
+ # Summary
2120
+ echo ""
2121
+ echo "Summary: $ok OK, $warn WARN, $err ERR"
2122
+
2123
+ # Exit code: 1 if any errors
2124
+ [ "$err" -gt 0 ] && exit 1
2125
+ return 0
2126
+ }
2127
+
2128
+ # --- do_uninstall() ---
2129
+ # Remove CortexHawk installation cleanly
2130
+ do_uninstall() {
2131
+ local target_cli="${TARGET_CLI:-claude}"
2132
+
2133
+ # Determine target paths
2134
+ local project_root
2135
+ if [ "$GLOBAL" = true ]; then
2136
+ project_root="$HOME"
2137
+ else
2138
+ project_root="$(pwd)"
2139
+ fi
2140
+
2141
+ local target=""
2142
+ local extra_dirs=""
2143
+ case "$target_cli" in
2144
+ claude) target="$project_root/.claude" ;;
2145
+ kimi) target="$project_root/.kimi" ;;
2146
+ codex) target="$project_root/.codex"; extra_dirs="$project_root/.agents" ;;
2147
+ *)
2148
+ echo "Error: unknown target '$target_cli'"
2149
+ exit 1
2150
+ ;;
2151
+ esac
2152
+
2153
+ if [ ! -d "$target" ] || [ ! -f "$target/.cortexhawk-manifest" ]; then
2154
+ echo "Error: no CortexHawk installation found at $target"
2155
+ exit 1
2156
+ fi
2157
+
2158
+ # Read manifest info
2159
+ local version="" profile=""
2160
+ version=$(grep -o '"version": "[^"]*"' "$target/.cortexhawk-manifest" 2>/dev/null | head -1 | cut -d'"' -f4)
2161
+ profile=$(grep -o '"profile": "[^"]*"' "$target/.cortexhawk-manifest" 2>/dev/null | head -1 | cut -d'"' -f4)
2162
+
2163
+ echo "CortexHawk Uninstall"
2164
+ echo "======================"
2165
+ echo " Target: $target_cli"
2166
+ echo " Location: $target"
2167
+ echo " Version: ${version:-unknown}"
2168
+ echo " Profile: ${profile:-unknown}"
2169
+ echo ""
2170
+
2171
+ # Inventory what will be removed
2172
+ echo "Will remove:"
2173
+ local components=(agents commands skills hooks modes mcp)
2174
+ for comp in "${components[@]}"; do
2175
+ if [ -d "$target/$comp" ]; then
2176
+ local count
2177
+ count=$(find "$target/$comp" -type f 2>/dev/null | wc -l | tr -d ' ')
2178
+ echo " - $target/$comp/ ($count files)"
2179
+ fi
2180
+ done
2181
+ [ -f "$target/settings.json" ] && echo " - $target/settings.json"
2182
+ [ -f "$target/MCP-SETUP.md" ] && echo " - $target/MCP-SETUP.md"
2183
+ [ -f "$target/.cortexhawk-manifest" ] && echo " - $target/.cortexhawk-manifest"
2184
+ [ -f "$target/git-workflow.conf" ] && echo " - $target/git-workflow.conf"
2185
+ [ -n "$extra_dirs" ] && [ -d "$extra_dirs" ] && echo " - $extra_dirs/"
2186
+
2187
+ # Check CLAUDE.md — only remove if matches template
2188
+ local claude_md="$project_root/CLAUDE.md"
2189
+ local remove_claude_md=false
2190
+ if [ -f "$claude_md" ] && [ -f "$SCRIPT_DIR/CLAUDE.md" ]; then
2191
+ local current_sum template_sum
2192
+ current_sum=$(compute_checksum "$claude_md")
2193
+ template_sum=$(compute_checksum "$SCRIPT_DIR/CLAUDE.md")
2194
+ if [ "$current_sum" = "$template_sum" ]; then
2195
+ echo " - CLAUDE.md (matches template)"
2196
+ remove_claude_md=true
2197
+ fi
2198
+ fi
2199
+
2200
+ # Check AGENTS.md — remove if generated by CortexHawk (Kimi/Codex targets)
2201
+ local agents_md="$project_root/AGENTS.md"
2202
+ local remove_agents_md=false
2203
+ if [ -f "$agents_md" ] && grep -q "CortexHawk" "$agents_md" 2>/dev/null; then
2204
+ case "$target_cli" in
2205
+ kimi|codex)
2206
+ echo " - AGENTS.md (generated by CortexHawk)"
2207
+ remove_agents_md=true
2208
+ ;;
2209
+ esac
2210
+ fi
2211
+
2212
+ echo ""
2213
+ echo "Will NOT remove:"
2214
+ echo " - $target/.cortexhawk-snapshots/ (preserved for rollback)"
2215
+ echo " - docs/ (user data)"
2216
+ echo " - .cortexhawk-team.yml (team config)"
2217
+ echo ""
2218
+
2219
+ # Pre-uninstall snapshot (saved to project root, not inside target)
2220
+ local snap_dir="$project_root/.cortexhawk-snapshots"
2221
+ if [ "$FORCE_MODE" != true ]; then
2222
+ printf "Create snapshot before uninstall? [Y/n] "
2223
+ read -r snap_confirm </dev/tty 2>/dev/null || snap_confirm="y"
2224
+ [ -z "$snap_confirm" ] && snap_confirm="y"
2225
+ else
2226
+ snap_confirm="y"
2227
+ fi
2228
+
2229
+ if [[ "$snap_confirm" =~ ^[Yy] ]]; then
2230
+ mkdir -p "$snap_dir"
2231
+ local snap_name
2232
+ snap_name=$(date +"%Y-%m-%d-%H%M%S")
2233
+ # Copy manifest as snapshot (lightweight)
2234
+ if [ -f "$target/.cortexhawk-manifest" ]; then
2235
+ cp "$target/.cortexhawk-manifest" "$snap_dir/${snap_name}-pre-uninstall.json"
2236
+ echo " Snapshot saved: $snap_dir/${snap_name}-pre-uninstall.json"
2237
+ fi
2238
+ fi
2239
+
2240
+ # Confirm uninstall
2241
+ if [ "$FORCE_MODE" != true ]; then
2242
+ printf "Proceed with uninstall? [y/N] "
2243
+ read -r confirm </dev/tty 2>/dev/null || confirm="n"
2244
+ if [[ ! "$confirm" =~ ^[Yy] ]]; then
2245
+ echo "Uninstall cancelled."
2246
+ exit 0
2247
+ fi
2248
+ fi
2249
+
2250
+ echo ""
2251
+
2252
+ # Remove CortexHawk components
2253
+ for comp in "${components[@]}"; do
2254
+ if [ -d "$target/$comp" ]; then
2255
+ rm -rf "$target/$comp"
2256
+ echo " Removed $target/$comp/"
2257
+ fi
2258
+ done
2259
+
2260
+ [ -f "$target/settings.json" ] && rm -f "$target/settings.json" && echo " Removed $target/settings.json"
2261
+ [ -f "$target/MCP-SETUP.md" ] && rm -f "$target/MCP-SETUP.md" && echo " Removed $target/MCP-SETUP.md"
2262
+ [ -f "$target/.cortexhawk-manifest" ] && rm -f "$target/.cortexhawk-manifest" && echo " Removed $target/.cortexhawk-manifest"
2263
+ [ -f "$target/git-workflow.conf" ] && rm -f "$target/git-workflow.conf" && echo " Removed $target/git-workflow.conf"
2264
+
2265
+ # Extra dirs (codex .agents/)
2266
+ if [ -n "$extra_dirs" ] && [ -d "$extra_dirs" ]; then
2267
+ rm -rf "$extra_dirs"
2268
+ echo " Removed $extra_dirs/"
2269
+ fi
2270
+
2271
+ # CLAUDE.md if matches template
2272
+ if [ "$remove_claude_md" = true ]; then
2273
+ rm -f "$claude_md"
2274
+ echo " Removed CLAUDE.md"
2275
+ fi
2276
+
2277
+ # AGENTS.md if generated by CortexHawk
2278
+ if [ "$remove_agents_md" = true ]; then
2279
+ rm -f "$agents_md"
2280
+ echo " Removed AGENTS.md"
2281
+ fi
2282
+
2283
+ # Clean empty target dir (but keep if snapshots exist inside)
2284
+ if [ -d "$target" ]; then
2285
+ # Keep if .cortexhawk-snapshots exists inside
2286
+ local remaining
2287
+ remaining=$(find "$target" -mindepth 1 -not -path "$target/.cortexhawk-snapshots*" 2>/dev/null | wc -l | tr -d ' ')
2288
+ if [ "$remaining" -eq 0 ]; then
2289
+ # Only snapshots left — keep them
2290
+ echo " Kept $target/.cortexhawk-snapshots/"
2291
+ fi
2292
+ fi
2293
+
2294
+ echo ""
2295
+ echo "Uninstall complete."
2296
+ if [ -f "$snap_dir/${snap_name}-pre-uninstall.json" ] 2>/dev/null; then
2297
+ echo " Rollback: bash install.sh --restore $snap_dir/${snap_name}-pre-uninstall.json"
2298
+ fi
2299
+ }
2300
+
2301
+ # --- do_list_hooks() ---
2302
+ do_list_hooks() {
2303
+ local hooks_json="$SCRIPT_DIR/hooks/hooks.json"
2304
+ local compose_file="$SCRIPT_DIR/hooks/compose.yml"
2305
+ if [ ! -f "$hooks_json" ]; then
2306
+ echo "Error: hooks.json not found"
2307
+ exit 1
2308
+ fi
2309
+ echo "CortexHawk Hooks"
2310
+ echo "================"
2311
+ echo ""
2312
+ printf " %-20s %-14s %-8s %s\n" "Name" "Event" "Status" "Description"
2313
+ printf " %-20s %-14s %-8s %s\n" "----" "-----" "------" "-----------"
2314
+ # Parse hooks.json with python3 for reliable JSON handling
2315
+ python3 << PYEOF
2316
+ import json
2317
+ with open("$hooks_json") as f:
2318
+ data = json.load(f)
2319
+ # Read compose.yml to detect disabled hooks (commented out)
2320
+ disabled = set()
2321
+ try:
2322
+ with open("$compose_file") as f:
2323
+ for line in f:
2324
+ stripped = line.strip()
2325
+ if stripped.startswith("# - "):
2326
+ disabled.add(stripped[4:].strip())
2327
+ except FileNotFoundError:
2328
+ pass
2329
+ for hook in data["hooks"]:
2330
+ name = hook["name"]
2331
+ event = hook["type"]
2332
+ desc = hook["description"]
2333
+ status = "OFF" if name in disabled else "ON"
2334
+ print(f" {name:<20} {event:<14} {status:<8} {desc}")
2335
+ PYEOF
2336
+ echo ""
2337
+ # Also show telemetry hooks not in hooks.json but in compose.yml
2338
+ for extra in agent-analytics session-telemetry; do
2339
+ if ! grep -q "\"$extra\"" "$hooks_json" 2>/dev/null; then
2340
+ local event="PostToolUse"
2341
+ [ "$extra" = "session-telemetry" ] && event="SessionEnd"
2342
+ local status="ON"
2343
+ grep -q "# - $extra" "$compose_file" 2>/dev/null && status="OFF"
2344
+ printf " %-20s %-14s %-8s %s\n" "$extra" "$event" "$status" "(telemetry hook)"
2345
+ fi
2346
+ done
2347
+ echo ""
2348
+ echo "Toggle: --enable-hook <name> | --disable-hook <name>"
2349
+ }
2350
+
2351
+ # --- do_toggle_hook() ---
2352
+ do_toggle_hook() {
2353
+ local hook_name="$1"
2354
+ local action="$2" # enable or disable
2355
+ local compose_file="$SCRIPT_DIR/hooks/compose.yml"
2356
+ if [ ! -f "$compose_file" ]; then
2357
+ echo "Error: compose.yml not found"
2358
+ exit 1
2359
+ fi
2360
+ # Verify hook exists in compose.yml (active or commented)
2361
+ if ! grep -qE "^ - ${hook_name}$|^ # - ${hook_name}$" "$compose_file"; then
2362
+ echo "Error: hook '$hook_name' not found in compose.yml"
2363
+ echo "Run --list-hooks to see available hooks"
2364
+ exit 1
2365
+ fi
2366
+ if [ "$action" = "disable" ]; then
2367
+ if grep -q "^ # - ${hook_name}$" "$compose_file"; then
2368
+ echo "Hook '$hook_name' is already disabled"
2369
+ return 0
2370
+ fi
2371
+ sed -i "s/^ - ${hook_name}$/ # - ${hook_name}/" "$compose_file"
2372
+ echo "Disabled hook: $hook_name"
2373
+ else
2374
+ if grep -q "^ - ${hook_name}$" "$compose_file"; then
2375
+ echo "Hook '$hook_name' is already enabled"
2376
+ return 0
2377
+ fi
2378
+ sed -i "s/^ # - ${hook_name}$/ - ${hook_name}/" "$compose_file"
2379
+ echo "Enabled hook: $hook_name"
2380
+ fi
2381
+ # Regenerate settings.json if target exists
2382
+ local target="${TARGET:-.claude}"
2383
+ if [ -d "$target" ] && [ -f "$target/settings.json" ]; then
2384
+ local hooks_json
2385
+ hooks_json=$(generate_hooks_config "$compose_file" ".claude/hooks")
2386
+ if [ -n "$hooks_json" ]; then
2387
+ python3 << PYEOF
2388
+ import json
2389
+ with open('$target/settings.json') as f:
2390
+ settings = json.load(f)
2391
+ settings['hooks'] = json.loads('''$hooks_json''')
2392
+ with open('$target/settings.json', 'w') as f:
2393
+ json.dump(settings, f, indent=2)
2394
+ f.write('\n')
2395
+ print(' Regenerated settings.json')
2396
+ PYEOF
2397
+ fi
2398
+ fi
2399
+ }
2400
+
2401
+ # --- do_search_skills() ---
2402
+ # Search community skill registry for matching skills
2403
+ do_search_skills() {
2404
+ local keyword="$1"
2405
+
2406
+ echo "CortexHawk Skill Search"
2407
+ echo "========================"
2408
+ echo " Query: $keyword"
2409
+ echo ""
2410
+
2411
+ # Strategy: SkillsMP API if key available, else local REGISTRY.md
2412
+ if [ -n "${SKILLSMP_API_KEY:-}" ]; then
2413
+ _search_skillsmp "$keyword"
2414
+ else
2415
+ _search_registry "$keyword"
2416
+ fi
2417
+ return 0
2418
+ }
2419
+
2420
+ # --- SkillsMP API search (87k+ skills) ---
2421
+ _search_skillsmp() {
2422
+ local keyword="$1"
2423
+ local api_url="https://skillsmp.com/api/v1/skills/search"
2424
+ echo " Source: SkillsMP (87k+ skills)"
2425
+ echo ""
2426
+
2427
+ local tmp_response
2428
+ tmp_response=$(mktemp)
2429
+ local http_code
2430
+ http_code=$(curl -sS --max-time 15 -o "$tmp_response" -w "%{http_code}" \
2431
+ -H "Authorization: Bearer $SKILLSMP_API_KEY" \
2432
+ -H "Accept: application/json" \
2433
+ "${api_url}?q=$(printf '%s' "$keyword" | sed 's/ /%20/g')&limit=15" 2>/dev/null) || http_code="000"
2434
+
2435
+ if [ "$http_code" != "200" ]; then
2436
+ echo " SkillsMP API error (HTTP $http_code) — falling back to local registry"
2437
+ echo ""
2438
+ rm -f "$tmp_response"
2439
+ _search_registry "$keyword"
2440
+ return 0
2441
+ fi
2442
+
2443
+ # Parse JSON response with python3
2444
+ python3 << PYEOF
2445
+ import json, sys
2446
+ try:
2447
+ with open("$tmp_response") as f:
2448
+ data = json.load(f)
2449
+ container = data.get("data", {})
2450
+ skills = container.get("skills", [])
2451
+ pagination = container.get("pagination", {})
2452
+ total = pagination.get("total", len(skills))
2453
+ if not skills:
2454
+ print(" No skills found matching '$keyword'")
2455
+ print("")
2456
+ print(" Try broader terms or check https://skillsmp.com")
2457
+ sys.exit(0)
2458
+ print(f" Found {total} skill(s) (showing {len(skills)}):")
2459
+ print("")
2460
+ print(f" {'Name':<28} {'Author':<18} {'Stars':<7} {'Description'}")
2461
+ print(f" {'----':<28} {'------':<18} {'-----':<7} {'-----------'}")
2462
+ for s in skills:
2463
+ name = s.get("name", "unknown")[:27]
2464
+ author = s.get("author", "")[:17]
2465
+ stars = s.get("stars", 0)
2466
+ star_str = str(stars) if stars else "-"
2467
+ desc = s.get("description", "")[:55]
2468
+ print(f" {name:<28} {author:<18} {star_str:<7} {desc}")
2469
+ print("")
2470
+ for s in skills:
2471
+ url = s.get("githubUrl", "")
2472
+ if url and "github.com" in url:
2473
+ parts = url.replace("https://github.com/", "").split("/")
2474
+ if len(parts) >= 2:
2475
+ print(f" Install: ./install.sh --add-skill {parts[0]}/{parts[1]}")
2476
+ break
2477
+ print(f" Browse all: https://skillsmp.com/?q=$keyword")
2478
+ except Exception as e:
2479
+ print(f" Error parsing response: {e}")
2480
+ print(" Falling back to local registry")
2481
+ PYEOF
2482
+
2483
+ rm -f "$tmp_response"
2484
+ }
2485
+
2486
+ # --- Local REGISTRY.md search (fallback) ---
2487
+ _search_registry() {
2488
+ local keyword="$1"
2489
+ local registry_url="https://raw.githubusercontent.com/Spechawk94/CortexHawk/main/REGISTRY.md"
2490
+ echo " Source: local REGISTRY.md"
2491
+ echo " Tip: export SKILLSMP_API_KEY=sk_live_xxx for 87k+ skills via SkillsMP"
2492
+ echo ""
2493
+
2494
+ local registry_file=""
2495
+ if [ -f "$SCRIPT_DIR/REGISTRY.md" ]; then
2496
+ registry_file="$SCRIPT_DIR/REGISTRY.md"
2497
+ else
2498
+ local tmp_file
2499
+ tmp_file=$(mktemp)
2500
+ if ! curl -sS --max-time 10 "$registry_url" > "$tmp_file" 2>/dev/null; then
2501
+ echo "Error: could not reach skill registry"
2502
+ rm -f "$tmp_file"
2503
+ exit 1
2504
+ fi
2505
+ registry_file="$tmp_file"
2506
+ fi
2507
+
2508
+ local results
2509
+ results=$(grep -i "$keyword" "$registry_file" | grep '^|' | grep -v '^| Name' | grep -v '^|---' || true)
2510
+
2511
+ if [ -z "$results" ]; then
2512
+ echo " No skills found matching '$keyword'"
2513
+ echo ""
2514
+ echo " Submit your skill: open a PR adding a row to REGISTRY.md"
2515
+ else
2516
+ local count
2517
+ count=$(echo "$results" | wc -l | tr -d ' ')
2518
+ echo " Found $count skill(s):"
2519
+ echo ""
2520
+ echo "| Name | Category | Description | Install |"
2521
+ echo "|---|---|---|---|"
2522
+ echo "$results" | while IFS='|' read -r _ name category desc _ url _; do
2523
+ name=$(echo "$name" | xargs)
2524
+ category=$(echo "$category" | xargs)
2525
+ desc=$(echo "$desc" | xargs)
2526
+ url=$(echo "$url" | xargs)
2527
+ printf "| %s | %s | %s | \`--add-skill %s\` |\n" "$name" "$category" "$desc" "$url"
2528
+ done
2529
+ echo ""
2530
+ echo "Install with: ./install.sh --add-skill <url>"
2531
+ fi
2532
+
2533
+ [ -n "${tmp_file:-}" ] && rm -f "$tmp_file" || true
2534
+ }
2535
+
2536
+ # --- do_add_skill() ---
2537
+ # Install a community skill from GitHub
2538
+ do_add_skill() {
2539
+ local url="$1"
2540
+
2541
+ # Normalize URL: user/repo → https://github.com/user/repo
2542
+ # Also support file:// and local paths for testing
2543
+ if [[ "$url" =~ ^/ ]]; then
2544
+ # Local path
2545
+ url="file://$url"
2546
+ elif [[ ! "$url" =~ ^(https?|file):// ]]; then
2547
+ url="https://github.com/$url"
2548
+ fi
2549
+
2550
+ # Extract repo name for skill directory
2551
+ local repo_name
2552
+ repo_name=$(basename "$url" .git)
2553
+
2554
+ echo "CortexHawk Add Skill"
2555
+ echo "====================="
2556
+ echo " Source: $url"
2557
+ echo " Skill: $repo_name"
2558
+ echo ""
2559
+
2560
+ # Determine target directory
2561
+ if [ "$GLOBAL" = true ]; then
2562
+ TARGET="$HOME/.claude"
2563
+ else
2564
+ TARGET="$(pwd)/.claude"
2565
+ fi
2566
+
2567
+ local skill_dir="$TARGET/skills/community/$repo_name"
2568
+
2569
+ # Check if already installed
2570
+ if [ -d "$skill_dir" ]; then
2571
+ echo "Error: skill '$repo_name' already installed at $skill_dir"
2572
+ echo "Remove it first with: rm -rf $skill_dir"
2573
+ exit 1
2574
+ fi
2575
+
2576
+ # Clone to temp directory
2577
+ local tmp_dir
2578
+ tmp_dir=$(mktemp -d)
2579
+ echo " Cloning repository..."
2580
+
2581
+ if ! git clone --depth 1 --quiet "$url" "$tmp_dir/repo" 2>/dev/null; then
2582
+ echo "Error: failed to clone $url"
2583
+ echo "Check that the URL is correct and the repo is public"
2584
+ rm -rf "$tmp_dir"
2585
+ exit 1
2586
+ fi
2587
+
2588
+ # Validate structure: must have SKILL.md
2589
+ if [ ! -f "$tmp_dir/repo/SKILL.md" ]; then
2590
+ echo "Error: invalid skill — SKILL.md not found"
2591
+ echo "A valid CortexHawk skill must have a SKILL.md file"
2592
+ rm -rf "$tmp_dir"
2593
+ exit 1
2594
+ fi
2595
+
2596
+ # Security warning if scripts present
2597
+ if find "$tmp_dir/repo" -name "*.sh" -o -name "*.py" 2>/dev/null | grep -q .; then
2598
+ echo " Warning: this skill contains executable scripts"
2599
+ echo " Review them before use: $skill_dir/scripts/"
2600
+ fi
2601
+
2602
+ # Create community directory if needed
2603
+ mkdir -p "$TARGET/skills/community"
2604
+
2605
+ # Copy skill (exclude .git)
2606
+ mkdir -p "$skill_dir"
2607
+ find "$tmp_dir/repo" -mindepth 1 -maxdepth 1 ! -name '.git' -exec cp -r {} "$skill_dir/" \;
2608
+
2609
+ # Cleanup
2610
+ rm -rf "$tmp_dir"
2611
+
2612
+ # Check requires: dependencies
2613
+ local requires
2614
+ requires=$(head -10 "$skill_dir/SKILL.md" | sed -n '/^---$/,/^---$/{ /^requires:/{ s/^requires: //; p; } }')
2615
+ if [ -n "$requires" ]; then
2616
+ local missing=""
2617
+ for dep in $requires; do
2618
+ if [ ! -d "$TARGET/skills/$dep" ]; then
2619
+ missing="${missing:+$missing, }$dep"
2620
+ fi
2621
+ done
2622
+ if [ -n "$missing" ]; then
2623
+ echo " Warning: missing dependencies: $missing"
2624
+ echo " Install them with: --add-skill or a profile that includes them"
2625
+ fi
2626
+ fi
2627
+
2628
+ echo ""
2629
+ echo "Skill installed successfully!"
2630
+ echo " Location: $skill_dir"
2631
+ echo ""
2632
+ echo "Use the skill by referencing it in your prompts."
2633
+ }
2634
+
2635
+ # --- parse_team_yaml() ---
2636
+ parse_team_yaml() {
2637
+ local file="$1"
2638
+ local current_section=""
2639
+ TEAM_VERSION="" TEAM_PROFILE=""
2640
+ TEAM_SKILLS="" TEAM_HOOKS="" TEAM_MODES=""
2641
+ TEAM_GIT_BRANCHING="" TEAM_GIT_COMMIT_CONVENTION="" TEAM_GIT_PR_PREFERENCE="" TEAM_GIT_AUTO_PUSH=""
2642
+
2643
+ while IFS= read -r line; do
2644
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
2645
+ [[ "$line" =~ ^[[:space:]]*$ ]] && continue
2646
+ # List item
2647
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+(.*) ]]; then
2648
+ local val="${BASH_REMATCH[1]}"
2649
+ case "$current_section" in
2650
+ skills) TEAM_SKILLS="${TEAM_SKILLS:+$TEAM_SKILLS$'\n'}$val" ;;
2651
+ hooks) TEAM_HOOKS="${TEAM_HOOKS:+$TEAM_HOOKS$'\n'}$val" ;;
2652
+ modes) TEAM_MODES="${TEAM_MODES:+$TEAM_MODES$'\n'}$val" ;;
2653
+ esac
2654
+ continue
2655
+ fi
2656
+ # Nested key under git_workflow
2657
+ if [[ "$line" =~ ^[[:space:]]+([a-z_]+):[[:space:]]*(.*) ]] && [ "$current_section" = "git_workflow" ]; then
2658
+ local key="${BASH_REMATCH[1]}" val="${BASH_REMATCH[2]//\"/}"
2659
+ case "$key" in
2660
+ branching) TEAM_GIT_BRANCHING="$val" ;;
2661
+ commit_convention) TEAM_GIT_COMMIT_CONVENTION="$val" ;;
2662
+ pr_preference) TEAM_GIT_PR_PREFERENCE="$val" ;;
2663
+ auto_push) TEAM_GIT_AUTO_PUSH="$val" ;;
2664
+ esac
2665
+ continue
2666
+ fi
2667
+ # Top-level key
2668
+ if [[ "$line" =~ ^([a-z_]+):[[:space:]]*(.*) ]]; then
2669
+ local key="${BASH_REMATCH[1]}" val="${BASH_REMATCH[2]//\"/}"
2670
+ current_section="$key"
2671
+ case "$key" in
2672
+ version) TEAM_VERSION="$val" ;;
2673
+ profile) TEAM_PROFILE="$val" ;;
2674
+ esac
2675
+ fi
2676
+ done < "$file"
2677
+ }
2678
+
2679
+ # --- do_team_install() ---
2680
+ do_team_install() {
2681
+ local team_file=".cortexhawk-team.yml"
2682
+ local local_file=".cortexhawk-local.yml"
2683
+
2684
+ if [ ! -f "$team_file" ]; then
2685
+ echo "Error: $team_file not found in $(pwd)"
2686
+ echo "Create a .cortexhawk-team.yml file or use install.sh without --team"
2687
+ exit 1
2688
+ fi
2689
+
2690
+ echo "CortexHawk Team Install"
2691
+ echo "========================="
2692
+
2693
+ # Parse team config
2694
+ parse_team_yaml "$team_file"
2695
+ echo " Team config: $team_file (v$TEAM_VERSION)"
2696
+
2697
+ # Parse local overrides if present
2698
+ if [ -f "$local_file" ]; then
2699
+ local saved_profile="$TEAM_PROFILE" saved_skills="$TEAM_SKILLS"
2700
+ local saved_hooks="$TEAM_HOOKS" saved_modes="$TEAM_MODES"
2701
+ local saved_gb="$TEAM_GIT_BRANCHING" saved_gc="$TEAM_GIT_COMMIT_CONVENTION"
2702
+ local saved_gp="$TEAM_GIT_PR_PREFERENCE" saved_ga="$TEAM_GIT_AUTO_PUSH"
2703
+ parse_team_yaml "$local_file"
2704
+ [ -z "$TEAM_PROFILE" ] && TEAM_PROFILE="$saved_profile"
2705
+ [ -z "$TEAM_SKILLS" ] && TEAM_SKILLS="$saved_skills"
2706
+ [ -z "$TEAM_HOOKS" ] && TEAM_HOOKS="$saved_hooks"
2707
+ [ -z "$TEAM_MODES" ] && TEAM_MODES="$saved_modes"
2708
+ [ -z "$TEAM_GIT_BRANCHING" ] && TEAM_GIT_BRANCHING="$saved_gb"
2709
+ [ -z "$TEAM_GIT_COMMIT_CONVENTION" ] && TEAM_GIT_COMMIT_CONVENTION="$saved_gc"
2710
+ [ -z "$TEAM_GIT_PR_PREFERENCE" ] && TEAM_GIT_PR_PREFERENCE="$saved_gp"
2711
+ [ -z "$TEAM_GIT_AUTO_PUSH" ] && TEAM_GIT_AUTO_PUSH="$saved_ga"
2712
+ echo " Local overrides: $local_file"
2713
+ fi
2714
+
2715
+ echo " Profile: ${TEAM_PROFILE:-all}"
2716
+ echo ""
2717
+
2718
+ # Generate temporary profile JSON from skills list
2719
+ if [ -n "$TEAM_SKILLS" ]; then
2720
+ local tmp_profile="/tmp/cortexhawk-team-$$.json"
2721
+ printf '{\n "name": "team",\n "skills": [\n' > "$tmp_profile"
2722
+ local first=true
2723
+ while IFS= read -r skill; do
2724
+ [ -z "$skill" ] && continue
2725
+ if [ "$first" = true ]; then
2726
+ printf ' "%s"' "$skill" >> "$tmp_profile"
2727
+ first=false
2728
+ else
2729
+ printf ',\n "%s"' "$skill" >> "$tmp_profile"
2730
+ fi
2731
+ done <<< "$TEAM_SKILLS"
2732
+ printf '\n ]\n}\n' >> "$tmp_profile"
2733
+ PROFILE="team"
2734
+ PROFILE_FILE="$tmp_profile"
2735
+ elif [ -n "$TEAM_PROFILE" ] && [ "$TEAM_PROFILE" != "all" ]; then
2736
+ PROFILE="$TEAM_PROFILE"
2737
+ PROFILE_FILE="$SCRIPT_DIR/profiles/${TEAM_PROFILE}.json"
2738
+ if [ ! -f "$PROFILE_FILE" ]; then
2739
+ echo " Warning: profile '$TEAM_PROFILE' not found — installing all skills"
2740
+ PROFILE=""
2741
+ PROFILE_FILE=""
2742
+ fi
2743
+ fi
2744
+
2745
+ # Run standard install
2746
+ install_claude
2747
+
2748
+ # Filter hooks: remove those not in team list
2749
+ if [ -n "$TEAM_HOOKS" ]; then
2750
+ for hook_file in "$TARGET/hooks/"*; do
2751
+ [ -f "$hook_file" ] || continue
2752
+ local hook_name
2753
+ hook_name=$(basename "$hook_file" | sed 's/\.[^.]*$//')
2754
+ if ! echo "$TEAM_HOOKS" | grep -qx "$hook_name"; then
2755
+ rm -f "$hook_file"
2756
+ fi
2757
+ done
2758
+ local hook_count
2759
+ hook_count=$(echo "$TEAM_HOOKS" | wc -l | tr -d ' ')
2760
+ echo " Hooks: kept $hook_count (team-specified)"
2761
+ fi
2762
+
2763
+ # Filter modes: remove those not in team list
2764
+ if [ -n "$TEAM_MODES" ]; then
2765
+ for mode_file in "$TARGET/modes/"*; do
2766
+ [ -f "$mode_file" ] || continue
2767
+ local mode_name
2768
+ mode_name=$(basename "$mode_file" .md)
2769
+ if ! echo "$TEAM_MODES" | grep -qx "$mode_name"; then
2770
+ rm -f "$mode_file"
2771
+ fi
2772
+ done
2773
+ local mode_count
2774
+ mode_count=$(echo "$TEAM_MODES" | wc -l | tr -d ' ')
2775
+ echo " Modes: kept $mode_count (team-specified)"
2776
+ fi
2777
+
2778
+ # Write git-workflow.conf from team config
2779
+ if [ -n "$TEAM_GIT_BRANCHING" ] || [ -n "$TEAM_GIT_COMMIT_CONVENTION" ] || [ -n "$TEAM_GIT_PR_PREFERENCE" ] || [ -n "$TEAM_GIT_AUTO_PUSH" ]; then
2780
+ {
2781
+ echo "BRANCHING=${TEAM_GIT_BRANCHING:-direct-main}"
2782
+ echo "COMMIT_CONVENTION=${TEAM_GIT_COMMIT_CONVENTION:-conventional}"
2783
+ echo "PR_PREFERENCE=${TEAM_GIT_PR_PREFERENCE:-never}"
2784
+ echo "AUTO_PUSH=${TEAM_GIT_AUTO_PUSH:-false}"
2785
+ } > "$TARGET/git-workflow.conf"
2786
+ echo " Git workflow: configured from team preset"
2787
+ fi
2788
+
2789
+ # Update manifest after filtering
2790
+ write_manifest "$TARGET" "${PROFILE:-all}" "claude" false
2791
+
2792
+ echo ""
2793
+ echo "Team install complete."
2794
+ }
2795
+
2796
+ # --- install_claude() ---
2797
+ install_claude() {
2798
+ if [ "$GLOBAL" = true ]; then
2799
+ TARGET="$HOME/.claude"
2800
+ else
2801
+ TARGET="$(pwd)/.claude"
2802
+ fi
2803
+
2804
+ if [ "$DRY_RUN" = true ]; then
2805
+ echo "CortexHawk Dry Run (install)"
2806
+ echo "=============================="
2807
+ echo " Target: $TARGET"
2808
+ echo " Profile: ${PROFILE:-all}"
2809
+ echo ""
2810
+ echo "Would install:"
2811
+ for comp in agents commands hooks modes mcp; do
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"
2817
+ echo " settings.json"
2818
+ [ ! -f "$(pwd)/CLAUDE.md" ] && echo " CLAUDE.md"
2819
+ echo ""
2820
+ echo "No files were modified (dry run)."
2821
+ return
2822
+ fi
2823
+
2824
+ echo "Installing for Claude Code to project: $TARGET"
2825
+
2826
+ mkdir -p "$TARGET"/{agents,commands,skills,hooks,modes,mcp}
2827
+
2828
+ cp -r "$SCRIPT_DIR/agents/"* "$TARGET/agents/" 2>/dev/null || true
2829
+ # Copy agent personas from project root if present
2830
+ local project_root
2831
+ project_root="$(dirname "$TARGET")"
2832
+ if [ -d "$project_root/.cortexhawk-agents" ]; then
2833
+ cp -r "$project_root/.cortexhawk-agents/"*.md "$TARGET/agents/" 2>/dev/null || true
2834
+ local persona_count
2835
+ persona_count=$(find "$project_root/.cortexhawk-agents" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
2836
+ [ "$persona_count" -gt 0 ] && echo " Loaded $persona_count agent persona(s) from .cortexhawk-agents/"
2837
+ 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
+
2844
+ if [ ! -f "$TARGET/settings.json" ]; then
2845
+ # Generate settings.json with hooks from compose.yml
2846
+ local hooks_json
2847
+ hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
2848
+ if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
2849
+ # Build settings.json with generated hooks
2850
+ python3 -c "
2851
+ import json
2852
+ permissions = $(cat "$SCRIPT_DIR/settings.json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('permissions',{})))")
2853
+ hooks = $hooks_json
2854
+ with open('$TARGET/settings.json', 'w') as f:
2855
+ json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
2856
+ f.write('\n')
2857
+ "
2858
+ echo " Generated settings.json from hooks/compose.yml"
2859
+ else
2860
+ cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
2861
+ fi
2862
+ else
2863
+ echo "settings.json already exists — skipping (check manually for updates)"
2864
+ fi
2865
+
2866
+ PROJECT_ROOT="$(dirname "$TARGET")"
2867
+ if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
2868
+ cp "$SCRIPT_DIR/CLAUDE.md" "$PROJECT_ROOT/CLAUDE.md"
2869
+ else
2870
+ echo "CLAUDE.md already exists — skipping"
2871
+ fi
2872
+
2873
+ # Git workflow config (interactive in --init, defaults otherwise)
2874
+ if [ "$GLOBAL" = false ]; then
2875
+ if [ "$INIT_MODE" = true ]; then
2876
+ source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
2877
+ elif [ ! -f "$TARGET/git-workflow.conf" ]; then
2878
+ # Apply sensible defaults without asking
2879
+ GIT_BRANCHING="direct-main"
2880
+ GIT_COMMIT_CONVENTION="conventional"
2881
+ GIT_PR_PREFERENCE="on-demand"
2882
+ GIT_AUTO_PUSH="after-commit"
2883
+ GIT_WORK_BRANCH=""
2884
+ source "$SCRIPT_DIR/scripts/git-workflow-init.sh" "$PROJECT_ROOT" "$TARGET"
2885
+ fi
2886
+ fi
2887
+
2888
+ chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
2889
+
2890
+ # Write manifest for future updates
2891
+ write_manifest "$TARGET" "$PROFILE" "claude" false
2892
+
2893
+ # Create docs/ workspace for agent outputs (local only)
2894
+ if [ "$GLOBAL" = false ]; then
2895
+ create_docs_workspace "$(dirname "$TARGET")"
2896
+ fi
2897
+
2898
+ run_audit "$(dirname "$TARGET")"
2899
+ update_gitignore "$(dirname "$TARGET")" ".claude"
2900
+
2901
+ echo ""
2902
+ echo "CortexHawk installed successfully for Claude Code!"
2903
+ echo ""
2904
+ echo " 32 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
2905
+ echo ""
2906
+ do_quickstart
2907
+ echo ""
2908
+ echo " To activate: exit Claude Code (ctrl+c) and relaunch 'claude' in this directory."
2909
+ }
2910
+
2911
+ # --- install_kimi() ---
2912
+ install_kimi() {
2913
+ if [ "$GLOBAL" = true ]; then
2914
+ TARGET="$HOME/.kimi"
2915
+ echo "Installing for Kimi CLI globally to $TARGET"
2916
+ else
2917
+ TARGET="$(pwd)/.kimi"
2918
+ echo "Installing for Kimi CLI to project: $TARGET"
2919
+ fi
2920
+
2921
+ mkdir -p "$TARGET/skills"
2922
+
2923
+ # 1. Skills — direct copy (Kimi discovers .kimi/skills/ at project level)
2924
+ echo " Copying skills..."
2925
+ if [ -d "$SCRIPT_DIR/skills" ]; then
2926
+ copy_skills "$TARGET" "$PROFILE"
2927
+ fi
2928
+
2929
+ # 2. Commands → Skills (invocation via /skill:cmd-name)
2930
+ echo " Converting commands to skills..."
2931
+ for cmd_file in "$SCRIPT_DIR"/commands/*.md; do
2932
+ [ -f "$cmd_file" ] || continue
2933
+ local cmd_name
2934
+ cmd_name=$(basename "$cmd_file" .md)
2935
+ mkdir -p "$TARGET/skills/cmd-$cmd_name"
2936
+ cp "$cmd_file" "$TARGET/skills/cmd-$cmd_name/SKILL.md"
2937
+ done
2938
+ local cmd_count
2939
+ cmd_count=$(find "$TARGET/skills/cmd-"* -maxdepth 0 -type d 2>/dev/null | wc -l | tr -d ' ')
2940
+ echo " Converted $cmd_count commands to skills (invoke with /skill:cmd-name)"
2941
+
2942
+ # 3. Agents → Skills (full agent definitions, invocable via /skill:agent-name)
2943
+ echo " Converting agents to skills..."
2944
+ local agent_count=0
2945
+ for agent_file in "$SCRIPT_DIR"/agents/*.md; do
2946
+ [ -f "$agent_file" ] || continue
2947
+ local agent_name
2948
+ agent_name=$(basename "$agent_file" .md)
2949
+ mkdir -p "$TARGET/skills/agent-$agent_name"
2950
+ cp "$agent_file" "$TARGET/skills/agent-$agent_name/SKILL.md"
2951
+ agent_count=$((agent_count + 1))
2952
+ done
2953
+ echo " Converted $agent_count agents to skills (invoke with /skill:agent-name)"
2954
+
2955
+ # 4. Modes → Skills via shared function
2956
+ convert_modes_to_skills "$TARGET/skills" "modes/"
2957
+
2958
+ # 5. Hooks → Skills (manual invocation — Kimi has no lifecycle hooks)
2959
+ echo " Converting hooks to skills..."
2960
+ local hooks_count=0
2961
+ convert_hook_to_skill() {
2962
+ local name="$1" desc="$2" content="$3"
2963
+ mkdir -p "$TARGET/skills/hook-$name"
2964
+ {
2965
+ echo "---"
2966
+ echo "name: hook-$name"
2967
+ echo "description: $desc"
2968
+ echo "---"
2969
+ echo ""
2970
+ echo "$content"
2971
+ } > "$TARGET/skills/hook-$name/SKILL.md"
2972
+ hooks_count=$((hooks_count + 1))
2973
+ }
2974
+
2975
+ convert_hook_to_skill "self-review" "Review last changes for TODOs, secrets, debug artifacts" \
2976
+ "# Self-Review
2977
+
2978
+ Review the files you just modified for:
2979
+
2980
+ - [ ] TODO/FIXME/HACK/XXX markers
2981
+ - [ ] Hardcoded secrets, API keys, passwords, tokens
2982
+ - [ ] Debug statements (console.log, print(), breakpoint())
2983
+ - [ ] Temporary test code or commented-out blocks
2984
+
2985
+ Flag any issues found and suggest fixes."
2986
+
2987
+ convert_hook_to_skill "file-guard" "Check that sensitive files are not being accessed" \
2988
+ "# File Guard
2989
+
2990
+ NEVER read, edit, or write these files:
2991
+
2992
+ - \`.env\`, \`.env.*\` — environment secrets
2993
+ - \`*.pem\`, \`*.key\`, \`*.p12\`, \`*.pfx\`, \`*.keystore\` — certificates/keys
2994
+ - \`*credentials*\`, \`*secret*\` — credential files
2995
+ - \`id_rsa\`, \`id_ed25519\`, \`.ssh/*\` — SSH keys
2996
+
2997
+ If you are about to touch any of these, STOP and warn the user."
2998
+
2999
+ convert_hook_to_skill "commit-guard" "Validate conventional commit format and check for secrets" \
3000
+ "# Commit Guard
3001
+
3002
+ Before committing, verify:
3003
+
3004
+ 1. **Commit format**: \`type(scope): description\`
3005
+ - Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
3006
+ 2. **No secrets staged**: check staged files for API keys, passwords, tokens
3007
+ 3. **No .env files staged**: never commit .env files
3008
+
3009
+ If any check fails, warn and suggest a fix."
3010
+
3011
+ convert_hook_to_skill "dependency-check" "Alert when dependency files are modified" \
3012
+ "# Dependency Check
3013
+
3014
+ If you modified any dependency file (package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, pyproject.toml, pom.xml):
3015
+
3016
+ - [ ] Verify the change is intentional
3017
+ - [ ] Check for version conflicts
3018
+ - [ ] Run the appropriate install command
3019
+ - [ ] Update lockfile if needed"
3020
+
3021
+ convert_hook_to_skill "test-reminder" "Remind to update tests for modified source files" \
3022
+ "# Test Reminder
3023
+
3024
+ For each source file you modified, check:
3025
+
3026
+ - [ ] Do tests exist for this file?
3027
+ - [ ] Do the tests cover the changes you made?
3028
+ - [ ] Should new test cases be added?
3029
+
3030
+ If tests are missing or outdated, suggest what to test."
3031
+
3032
+ convert_hook_to_skill "branch-guard" "Check branch before pushing" \
3033
+ "# Branch Guard
3034
+
3035
+ Before pushing, verify:
3036
+
3037
+ - [ ] You are NOT on main/master (unless intended)
3038
+ - [ ] The branch name follows conventions
3039
+ - [ ] No force-push to protected branches"
3040
+
3041
+ echo " Converted $hooks_count hooks to skills (invoke with /skill:hook-name)"
3042
+
3043
+ # 6. AGENTS.md — in project root (Kimi auto-injects as KIMI_AGENTS_MD)
3044
+ echo " Generating AGENTS.md..."
3045
+ local agents_md
3046
+ if [ "$GLOBAL" = true ]; then
3047
+ agents_md="$HOME/AGENTS.md"
3048
+ else
3049
+ agents_md="$(pwd)/AGENTS.md"
3050
+ fi
3051
+ local commands_section
3052
+ commands_section="## Commands"$'\n'$'\n'"Commands are installed as skills. Invoke with /skill:cmd-name:"$'\n'
3053
+ for cmd_file in "$SCRIPT_DIR"/commands/*.md; do
3054
+ [ -f "$cmd_file" ] || continue
3055
+ local name desc
3056
+ name=$(basename "$cmd_file" .md)
3057
+ desc=$(grep '^description:' "$cmd_file" | sed 's/description: *//')
3058
+ commands_section="${commands_section}"$'\n'"- \`/skill:cmd-$name\` — $desc"
3059
+ done
3060
+ generate_agents_md "$agents_md" "optimized agents, skills, commands, and modes for Kimi CLI." "$commands_section"
3061
+
3062
+ # 7. MCP — optional reference (don't install — crashes if servers missing)
3063
+ if command -v npx &>/dev/null; then
3064
+ echo " MCP: creating setup reference..."
3065
+ {
3066
+ echo "# CortexHawk MCP Servers (Optional)"
3067
+ echo ""
3068
+ echo "Install manually:"
3069
+ echo ""
3070
+ for mcp_file in "$SCRIPT_DIR"/mcp/*.json; do
3071
+ [ -f "$mcp_file" ] || continue
3072
+ local server_name mcp_cmd mcp_args
3073
+ server_name=$(basename "$mcp_file" .json)
3074
+ mcp_cmd=$(grep '"command"' "$mcp_file" | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
3075
+ mcp_args=$(grep '"args"' "$mcp_file" | sed 's/.*"args"[[:space:]]*:[[:space:]]*\(\[.*\]\).*/\1/' | tr -d '[]"' | tr ',' ' ')
3076
+ echo " kimi mcp add --transport stdio $server_name -- $mcp_cmd $mcp_args"
3077
+ done
3078
+ echo ""
3079
+ echo "List installed: kimi mcp list"
3080
+ echo "Remove: kimi mcp remove <name>"
3081
+ } > "$TARGET/MCP-SETUP.md"
3082
+ else
3083
+ echo " MCP: npx not found — install Node.js for MCP servers (see MCP-SETUP.md)"
3084
+ echo "# CortexHawk MCP Servers — requires Node.js (npx)" > "$TARGET/MCP-SETUP.md"
3085
+ fi
3086
+
3087
+ # 8. Create docs/ workspace (local only)
3088
+ if [ "$GLOBAL" = false ]; then
3089
+ create_docs_workspace "$(dirname "$TARGET")"
3090
+ fi
3091
+
3092
+ # Write manifest
3093
+ write_manifest "$TARGET" "$PROFILE" "kimi" false
3094
+
3095
+ echo ""
3096
+ echo "CortexHawk installed successfully for Kimi CLI!"
3097
+ echo ""
3098
+ echo "Available: $agent_count agents, 36 skills, $cmd_count commands, 7 modes, $hooks_count hooks (all as skills)"
3099
+ echo ""
3100
+ echo "Agents: /skill:agent-planner, /skill:agent-reviewer, etc."
3101
+ echo "Commands: /skill:cmd-plan, /skill:cmd-build, /skill:cmd-test, etc."
3102
+ echo "Modes: /skill:modes/fast, /skill:modes/research, etc."
3103
+ echo "Hooks: /skill:hook-self-review, /skill:hook-file-guard, etc."
3104
+ echo "MCP: see .kimi/MCP-SETUP.md for optional server install"
3105
+
3106
+ run_audit "$(dirname "$TARGET")"
3107
+ update_gitignore "$(dirname "$TARGET")" ".kimi"
3108
+
3109
+ # Local install: auto-configure KIMI_SHARE_DIR for local sessions/config
3110
+ if [ "$GLOBAL" = false ]; then
3111
+ local project_root
3112
+ project_root="$(pwd)"
3113
+ local envrc="$project_root/.envrc"
3114
+ local share_export="export KIMI_SHARE_DIR=\"$project_root/.kimi\""
3115
+
3116
+ echo ""
3117
+ echo " Skills are auto-discovered from .kimi/skills/ (no action needed)."
3118
+
3119
+ # Check if KIMI_SHARE_DIR already configured
3120
+ if grep -q "KIMI_SHARE_DIR" "$envrc" 2>/dev/null; then
3121
+ green " KIMI_SHARE_DIR already in .envrc"
3122
+ elif command -v direnv &>/dev/null; then
3123
+ # direnv available — auto-create .envrc
3124
+ echo "$share_export" >> "$envrc"
3125
+ direnv allow "$project_root" 2>/dev/null
3126
+ green " Created .envrc with KIMI_SHARE_DIR (direnv detected)"
3127
+ # Add .envrc to gitignore if not already there
3128
+ if [ -f "$project_root/.gitignore" ] && ! grep -qx ".envrc" "$project_root/.gitignore" 2>/dev/null; then
3129
+ echo ".envrc" >> "$project_root/.gitignore"
3130
+ fi
3131
+ else
3132
+ # No direnv — create .envrc anyway + suggest manual source
3133
+ echo "$share_export" >> "$envrc"
3134
+ yellow " Created .envrc with KIMI_SHARE_DIR"
3135
+ echo " To activate: source .envrc (or install direnv for auto-load)"
3136
+ if [ -f "$project_root/.gitignore" ] && ! grep -qx ".envrc" "$project_root/.gitignore" 2>/dev/null; then
3137
+ echo ".envrc" >> "$project_root/.gitignore"
3138
+ fi
3139
+ fi
3140
+ fi
3141
+
3142
+ echo ""
3143
+ echo " To activate: exit Kimi (ctrl+d) and relaunch 'kimi' in this directory."
3144
+ }
3145
+
3146
+ # --- do_test_hooks() ---
3147
+ do_test_hooks() {
3148
+ if [ "$GLOBAL" = true ]; then
3149
+ TARGET="$HOME/.claude"
3150
+ else
3151
+ TARGET="$(pwd)/.claude"
3152
+ fi
3153
+
3154
+ local hooks_dir="$TARGET/hooks"
3155
+ if [ ! -d "$hooks_dir" ]; then
3156
+ echo "Error: no hooks directory at $hooks_dir"
3157
+ exit 1
3158
+ fi
3159
+
3160
+ echo "CortexHawk Hook Test"
3161
+ echo "======================"
3162
+ echo " Hooks: $hooks_dir"
3163
+ echo ""
3164
+
3165
+ local ok=0 fail=0
3166
+ local tmpfile="/tmp/.cortexhawk-hooktest-$$"
3167
+ echo "test file content" > "$tmpfile"
3168
+
3169
+ for hook in "$hooks_dir"/*.sh; do
3170
+ [ -f "$hook" ] || continue
3171
+ local name
3172
+ name=$(basename "$hook" .sh)
3173
+
3174
+ # Skip non-hook files and codex-dispatcher (requires jq + Codex payload)
3175
+ [ "$name" = "codex-dispatcher" ] && continue
3176
+ case "$name" in compose*|hooks*) continue ;; esac
3177
+
3178
+ # Generate synthetic input based on hook type
3179
+ local input=""
3180
+ case "$name" in
3181
+ file-guard|self-review|dependency-check|test-reminder|agent-analytics)
3182
+ input="{\"session_id\":\"test\",\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$tmpfile\"}}"
3183
+ ;;
3184
+ branch-guard|commit-guard)
3185
+ input='{"session_id":"test","tool_name":"Bash","tool_input":{"command":"echo test"}}'
3186
+ ;;
3187
+ session-telemetry)
3188
+ input='{"session_id":"test-hook-check"}'
3189
+ ;;
3190
+ session-start)
3191
+ input=""
3192
+ ;;
3193
+ *)
3194
+ input=""
3195
+ ;;
3196
+ esac
3197
+
3198
+ # Execute hook with timeout
3199
+ local output exit_code
3200
+ if [ -n "$input" ]; then
3201
+ output=$(echo "$input" | timeout 10 bash "$hook" 2>&1)
3202
+ exit_code=$?
3203
+ else
3204
+ output=$(timeout 10 bash "$hook" < /dev/null 2>&1)
3205
+ exit_code=$?
3206
+ fi
3207
+
3208
+ # Report — exit 0=OK, exit 2=BLOCK (legitimate), 124=timeout, other=FAIL
3209
+ if [ "$exit_code" -eq 0 ]; then
3210
+ if [ -n "$output" ]; then
3211
+ local first_line
3212
+ first_line=$(echo "$output" | head -1 | cut -c1-60)
3213
+ printf " [OK] %-25s %s\n" "$name" "$first_line"
3214
+ else
3215
+ printf " [OK] %s\n" "$name"
3216
+ fi
3217
+ ok=$((ok + 1))
3218
+ elif [ "$exit_code" -eq 2 ]; then
3219
+ local block_line
3220
+ block_line=$(echo "$output" | head -1 | cut -c1-60)
3221
+ printf " [BLOCK] %-24s %s\n" "$name" "$block_line"
3222
+ ok=$((ok + 1))
3223
+ elif [ "$exit_code" -eq 124 ]; then
3224
+ printf " [FAIL] %-25s timeout (>10s)\n" "$name"
3225
+ fail=$((fail + 1))
3226
+ else
3227
+ local err_line
3228
+ err_line=$(echo "$output" | head -1 | cut -c1-60)
3229
+ printf " [FAIL] %-25s exit %d: %s\n" "$name" "$exit_code" "$err_line"
3230
+ fail=$((fail + 1))
3231
+ fi
3232
+ done
3233
+
3234
+ rm -f "$tmpfile"
3235
+
3236
+ echo ""
3237
+ echo "Summary: $ok OK, $fail FAIL"
3238
+ [ "$fail" -gt 0 ] && exit 1
3239
+ exit 0
3240
+ }
3241
+
3242
+ # --- do_quickstart() ---
3243
+ do_quickstart() {
3244
+ echo ""
3245
+ echo "CortexHawk Quick Start"
3246
+ echo "========================"
3247
+ echo ""
3248
+ echo " 5 things to try right now:"
3249
+ echo ""
3250
+ echo " 1. /check — pre-commit quality gate (lint + test + scan → GO/NO-GO)"
3251
+ echo " 2. /chain default — full pipeline: plan → build → test → review"
3252
+ echo " 3. /pulse — project health dashboard with agent analytics"
3253
+ echo " 4. /context set db=postgres — share persistent context with all 20 agents"
3254
+ echo " 5. --search react — discover 87k+ community skills via SkillsMP"
3255
+ echo ""
3256
+ echo " Other essentials:"
3257
+ echo " /plan <feature> — break a feature into tasks"
3258
+ echo " /scan — full security audit"
3259
+ echo " /task #N — execute a backlog item end-to-end"
3260
+ echo " /brainstorm <topic> — structured ideation session"
3261
+ echo ""
3262
+ echo " Full docs: README.md | All commands: /help"
3263
+ echo ""
3264
+ }
3265
+
3266
+ # --- do_stats() ---
3267
+ do_stats() {
3268
+ if [ "$GLOBAL" = true ]; then
3269
+ TARGET="$HOME/.claude"
3270
+ else
3271
+ TARGET="$(pwd)/.claude"
3272
+ fi
3273
+
3274
+ local manifest="$TARGET/.cortexhawk-manifest"
3275
+ if [ ! -f "$manifest" ]; then
3276
+ echo "Error: no CortexHawk installation found at $TARGET"
3277
+ echo " Run ./install.sh first."
3278
+ exit 1
3279
+ fi
3280
+
3281
+ local version profile target_cli install_date update_date
3282
+ version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
3283
+ profile=$(grep '"profile"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
3284
+ target_cli=$(grep '"target"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
3285
+ update_date=$(grep '"update_date"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
3286
+
3287
+ # Count components from source
3288
+ local skills_count agents_count commands_count modes_count hooks_count packs_count
3289
+ skills_count=$(find "$SCRIPT_DIR/skills" -name "SKILL.md" 2>/dev/null | wc -l | tr -d ' ')
3290
+ agents_count=$(find "$SCRIPT_DIR/agents" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
3291
+ commands_count=$(find "$SCRIPT_DIR/commands" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
3292
+ modes_count=$(find "$SCRIPT_DIR/modes" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
3293
+ hooks_count=$(find "$TARGET/hooks" -name "*.sh" ! -name "codex-*" 2>/dev/null | wc -l | tr -d ' ')
3294
+ packs_count=$(grep -c '^| [a-z]' "$SCRIPT_DIR/PACKS.md" 2>/dev/null || echo "0")
3295
+
3296
+ # Count personas
3297
+ local personas_count=0
3298
+ if [ -d ".cortexhawk-agents" ]; then
3299
+ personas_count=$(find ".cortexhawk-agents" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
3300
+ fi
3301
+
3302
+ # Active hooks from compose.yml
3303
+ local active_hooks=0 disabled_hooks=0
3304
+ local compose="$TARGET/hooks/compose.yml"
3305
+ if [ -f "$compose" ]; then
3306
+ active_hooks=$(grep -c '^ *- ' "$compose" 2>/dev/null || echo "0")
3307
+ disabled_hooks=$(grep -c '^ *# *- ' "$compose" 2>/dev/null || echo "0")
3308
+ fi
3309
+
3310
+ # SkillsMP status
3311
+ local skillsmp_status="not configured"
3312
+ if [ -n "${SKILLSMP_API_KEY:-}" ]; then
3313
+ skillsmp_status="connected (87k+ skills)"
3314
+ fi
3315
+
3316
+ # Snapshots count
3317
+ local snapshots_count=0
3318
+ local snapshots_dir="$TARGET/.cortexhawk-snapshots"
3319
+ if [ -d "$snapshots_dir" ]; then
3320
+ snapshots_count=$(find "$snapshots_dir" -name "*.json" 2>/dev/null | wc -l | tr -d ' ')
3321
+ fi
3322
+
3323
+ echo ""
3324
+ echo "CortexHawk Installation Stats"
3325
+ echo "==============================="
3326
+ printf " %-14s %s\n" "Version:" "$version"
3327
+ printf " %-14s %s\n" "Target:" "$target_cli"
3328
+ printf " %-14s %s\n" "Profile:" "$profile"
3329
+ printf " %-14s %s\n" "Skills:" "$skills_count installed"
3330
+ printf " %-14s %s active, %s disabled\n" "Hooks:" "$active_hooks" "$disabled_hooks"
3331
+ printf " %-14s %s\n" "Agents:" "$agents_count$([ "$personas_count" -gt 0 ] && echo " + $personas_count personas")"
3332
+ printf " %-14s %s\n" "Commands:" "$commands_count"
3333
+ printf " %-14s %s\n" "Modes:" "$modes_count"
3334
+ printf " %-14s %s available\n" "Packs:" "$packs_count"
3335
+ printf " %-14s %s\n" "SkillsMP:" "$skillsmp_status"
3336
+ printf " %-14s %s\n" "Last update:" "${update_date%T*}"
3337
+ printf " %-14s %s saved\n" "Snapshots:" "$snapshots_count"
3338
+ echo ""
3339
+ }
3340
+
3341
+ # --- do_check_update() ---
3342
+ do_check_update() {
3343
+ if [ "$GLOBAL" = true ]; then
3344
+ TARGET="$HOME/.claude"
3345
+ else
3346
+ TARGET="$(pwd)/.claude"
3347
+ fi
3348
+
3349
+ local manifest="$TARGET/.cortexhawk-manifest"
3350
+ if [ ! -f "$manifest" ]; then
3351
+ echo "Error: no CortexHawk installation found at $TARGET"
3352
+ echo " Run ./install.sh first."
3353
+ exit 1
3354
+ fi
3355
+
3356
+ local installed_version
3357
+ installed_version=$(grep '"version"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
3358
+ local update_date
3359
+ update_date=$(grep '"update_date"' "$manifest" | sed 's/.*: *"\([^"]*\)".*/\1/')
3360
+
3361
+ echo ""
3362
+ echo "CortexHawk Update Check"
3363
+ echo "========================="
3364
+ echo " Installed: v$installed_version (updated ${update_date%T*})"
3365
+
3366
+ # Fetch latest version from source
3367
+ local source_type
3368
+ source_type=$(detect_source_type)
3369
+ local latest_version=""
3370
+ local changelog_path="$SCRIPT_DIR/CHANGELOG.md"
3371
+
3372
+ if [ "$source_type" = "git" ]; then
3373
+ echo " Checking for updates..."
3374
+ git -C "$SCRIPT_DIR" fetch --quiet 2>/dev/null || true
3375
+
3376
+ # Check if local is behind remote
3377
+ local local_hash remote_hash
3378
+ local_hash=$(git -C "$SCRIPT_DIR" rev-parse HEAD 2>/dev/null)
3379
+ remote_hash=$(git -C "$SCRIPT_DIR" rev-parse origin/main 2>/dev/null || echo "$local_hash")
3380
+
3381
+ if [ "$local_hash" = "$remote_hash" ]; then
3382
+ latest_version="$installed_version"
3383
+ else
3384
+ # Get version from remote CHANGELOG
3385
+ latest_version=$(git -C "$SCRIPT_DIR" show origin/main:CHANGELOG.md 2>/dev/null | grep -m1 '## \[' | sed 's/.*\[\([^]]*\)\].*/\1/')
3386
+ [ -z "$latest_version" ] && latest_version="$installed_version"
3387
+ fi
3388
+ else
3389
+ latest_version=$(get_version)
3390
+ fi
3391
+
3392
+ printf " %-10s v%s\n" "Latest:" "$latest_version"
3393
+ echo ""
3394
+
3395
+ if [ "$installed_version" = "$latest_version" ]; then
3396
+ echo " You're up to date!"
3397
+ echo ""
3398
+ exit 0
3399
+ fi
3400
+
3401
+ # Show what changed between installed and latest
3402
+ echo " What's new since v$installed_version:"
3403
+ echo " ─────────────────────────────────"
3404
+
3405
+ # Extract changelog entries between installed version and latest
3406
+ local in_section=false
3407
+ local stop_pattern="## \\[$installed_version\\]"
3408
+ while IFS= read -r line; do
3409
+ if [ "$in_section" = false ]; then
3410
+ if echo "$line" | grep -q '^## \['; then
3411
+ in_section=true
3412
+ fi
3413
+ [ "$in_section" = false ] && continue
3414
+ fi
3415
+ # Stop when we reach the installed version
3416
+ if echo "$line" | grep -q "$stop_pattern"; then
3417
+ break
3418
+ fi
3419
+ # Skip empty lines at the start
3420
+ [ -z "$line" ] && continue
3421
+ # Print changelog lines with indent
3422
+ echo " $line"
3423
+ done < "$changelog_path"
3424
+
3425
+ echo ""
3426
+ echo " To update: ./install.sh --update"
3427
+ echo " Preview: ./install.sh --update --dry-run"
3428
+ echo ""
3429
+ }
3430
+
3431
+ # --- do_publish_skill() ---
3432
+ do_publish_skill() {
3433
+ local skill_path="$1"
3434
+
3435
+ # Resolve to absolute path
3436
+ if [[ ! "$skill_path" =~ ^/ ]]; then
3437
+ skill_path="$(pwd)/$skill_path"
3438
+ fi
3439
+
3440
+ # Validate: directory exists
3441
+ if [ ! -d "$skill_path" ]; then
3442
+ echo "Error: directory not found: $skill_path"
3443
+ exit 1
3444
+ fi
3445
+
3446
+ # Validate: SKILL.md exists
3447
+ local skill_md="$skill_path/SKILL.md"
3448
+ if [ ! -f "$skill_md" ]; then
3449
+ echo "Error: no SKILL.md found in $skill_path"
3450
+ echo " A valid skill must have a SKILL.md with frontmatter (name, description, category)"
3451
+ exit 1
3452
+ fi
3453
+
3454
+ # Extract metadata from SKILL.md frontmatter
3455
+ local skill_name skill_desc skill_category
3456
+ skill_name=$(sed -n '/^---$/,/^---$/{ /^name:/{ s/^name: *//; p; } }' "$skill_md")
3457
+ skill_desc=$(sed -n '/^---$/,/^---$/{ /^description:/{ s/^description: *//; p; } }' "$skill_md")
3458
+ skill_category=$(sed -n '/^---$/,/^---$/{ /^category:/{ s/^category: *//; p; } }' "$skill_md" || true)
3459
+
3460
+ if [ -z "$skill_name" ]; then
3461
+ echo "Error: SKILL.md missing 'name' in frontmatter"
3462
+ exit 1
3463
+ fi
3464
+ if [ -z "$skill_desc" ]; then
3465
+ echo "Error: SKILL.md missing 'description' in frontmatter"
3466
+ exit 1
3467
+ fi
3468
+
3469
+ # Check gh CLI
3470
+ if ! command -v gh &>/dev/null; then
3471
+ echo "Error: GitHub CLI (gh) is required for --publish-skill"
3472
+ echo " Install: https://cli.github.com/"
3473
+ exit 1
3474
+ fi
3475
+
3476
+ if ! gh auth status &>/dev/null; then
3477
+ echo "Error: not logged in to GitHub"
3478
+ echo " Run: gh auth login"
3479
+ exit 1
3480
+ fi
3481
+
3482
+ local gh_user
3483
+ gh_user=$(gh api user --jq '.login' 2>/dev/null)
3484
+ local repo_name="cortexhawk-skill-${skill_name}"
3485
+
3486
+ echo ""
3487
+ echo "CortexHawk Publish Skill"
3488
+ echo "========================="
3489
+ echo " Skill: $skill_name"
3490
+ echo " Description: $skill_desc"
3491
+ echo " Category: ${skill_category:-unspecified}"
3492
+ echo " Source: $skill_path"
3493
+ echo ""
3494
+ echo " Will create: github.com/$gh_user/$repo_name"
3495
+ echo ""
3496
+ read -r -p "Proceed? [Y/n]: " confirm
3497
+ case "$confirm" in
3498
+ [nN]*) echo "Cancelled."; exit 0 ;;
3499
+ esac
3500
+
3501
+ # Create GitHub repo
3502
+ echo ""
3503
+ echo " Creating repository..."
3504
+ if gh repo view "$gh_user/$repo_name" &>/dev/null; then
3505
+ echo " Repository already exists: $gh_user/$repo_name"
3506
+ echo " Pushing latest files..."
3507
+ else
3508
+ gh repo create "$repo_name" --public --description "CortexHawk skill: $skill_desc" --clone=false
3509
+ fi
3510
+
3511
+ # Prepare temp directory with git
3512
+ local tmp_dir
3513
+ tmp_dir=$(mktemp -d)
3514
+ cp -r "$skill_path"/* "$tmp_dir/" 2>/dev/null || true
3515
+ cp -r "$skill_path"/.[!.]* "$tmp_dir/" 2>/dev/null || true
3516
+
3517
+ # Add README if not present
3518
+ if [ ! -f "$tmp_dir/README.md" ]; then
3519
+ printf '# %s\n\n%s\n\nA [CortexHawk](https://github.com/Spechawk94/CortexHawk) skill.\n\n## Install\n\n```bash\n./install.sh --add-skill %s/%s\n```\n' \
3520
+ "$skill_name" "$skill_desc" "$gh_user" "$repo_name" > "$tmp_dir/README.md"
3521
+ fi
3522
+
3523
+ # Init git and push
3524
+ cd "$tmp_dir"
3525
+ git init --quiet
3526
+ git add -A
3527
+ git commit --quiet -m "feat: publish $skill_name skill"
3528
+ git branch -M main
3529
+ git remote add origin "https://github.com/$gh_user/$repo_name.git"
3530
+ git push -u origin main --force --quiet 2>/dev/null
3531
+ cd - >/dev/null
3532
+
3533
+ # Cleanup
3534
+ rm -rf "$tmp_dir"
3535
+
3536
+ echo " Published to: https://github.com/$gh_user/$repo_name"
3537
+ echo ""
3538
+ echo " Others can install with:"
3539
+ echo " ./install.sh --add-skill $gh_user/$repo_name"
3540
+ echo ""
3541
+
3542
+ # Notify SkillsMP if API key is available
3543
+ if [ -n "${SKILLSMP_API_KEY:-}" ]; then
3544
+ echo " Notifying SkillsMP for indexation..."
3545
+ local response
3546
+ response=$(curl -s -w "%{http_code}" -o /dev/null \
3547
+ -X POST "https://skillsmp.com/api/v1/skills/submit" \
3548
+ -H "Authorization: Bearer $SKILLSMP_API_KEY" \
3549
+ -H "Content-Type: application/json" \
3550
+ -d "{\"github_url\": \"https://github.com/$gh_user/$repo_name\", \"name\": \"$skill_name\", \"description\": \"$skill_desc\"}" \
3551
+ 2>/dev/null || echo "000")
3552
+ if [ "$response" = "200" ] || [ "$response" = "201" ]; then
3553
+ echo " SkillsMP: submitted for indexation"
3554
+ else
3555
+ echo " SkillsMP: notification failed (HTTP $response) — skill is still published on GitHub"
3556
+ fi
3557
+ else
3558
+ echo " Tip: set SKILLSMP_API_KEY in .env to auto-submit for indexation"
3559
+ fi
3560
+ echo ""
3561
+ }
3562
+
3563
+ # --- do_demo() ---
3564
+ do_demo() {
3565
+ local demo_dir="/tmp/cortexhawk-demo-$$"
3566
+ mkdir -p "$demo_dir"
3567
+
3568
+ echo ""
3569
+ echo "CortexHawk Demo"
3570
+ echo "================="
3571
+ echo " Creating sandbox project at: $demo_dir"
3572
+ echo ""
3573
+
3574
+ # --- 1. Create a mini Express.js project ---
3575
+ cat > "$demo_dir/package.json" << 'PKGJSON'
3576
+ {
3577
+ "name": "demo-app",
3578
+ "version": "1.0.0",
3579
+ "description": "CortexHawk demo project",
3580
+ "main": "src/index.js",
3581
+ "scripts": {
3582
+ "start": "node src/index.js",
3583
+ "test": "echo \"no tests yet\" && exit 1"
3584
+ },
3585
+ "dependencies": {
3586
+ "express": "^4.18.0"
3587
+ }
3588
+ }
3589
+ PKGJSON
3590
+
3591
+ mkdir -p "$demo_dir/src"
3592
+
3593
+ # Main server file — has a SQL injection vulnerability (intentional)
3594
+ cat > "$demo_dir/src/index.js" << 'INDEXJS'
3595
+ const express = require('express');
3596
+ const app = express();
3597
+
3598
+ app.use(express.json());
3599
+
3600
+ // BUG: missing error handling on port binding
3601
+ const PORT = process.env.PORT || 3000;
3602
+
3603
+ // VULN: SQL injection — user input directly in query
3604
+ app.get('/users', (req, res) => {
3605
+ const name = req.query.name;
3606
+ const query = `SELECT * FROM users WHERE name = '${name}'`;
3607
+ // db.query(query) would execute here
3608
+ res.json({ query });
3609
+ });
3610
+
3611
+ // VULN: no input validation
3612
+ app.post('/login', (req, res) => {
3613
+ const { username, password } = req.body;
3614
+ if (username === 'admin' && password === 'admin123') {
3615
+ res.json({ token: 'hardcoded-secret-token' });
3616
+ } else {
3617
+ res.status(401).json({ error: 'Invalid credentials' });
3618
+ }
3619
+ });
3620
+
3621
+ // TODO: add proper authentication middleware
3622
+ // TODO: add rate limiting
3623
+
3624
+ app.listen(PORT, () => {
3625
+ console.log(`Server running on port ${PORT}`);
3626
+ });
3627
+ INDEXJS
3628
+
3629
+ # A utility with a bug
3630
+ cat > "$demo_dir/src/utils.js" << 'UTILSJS'
3631
+ // BUG: off-by-one error in pagination
3632
+ function paginate(items, page, perPage) {
3633
+ const start = page * perPage; // should be (page - 1) * perPage
3634
+ const end = start + perPage;
3635
+ return {
3636
+ data: items.slice(start, end),
3637
+ total: items.length,
3638
+ page,
3639
+ totalPages: Math.ceil(items.length / perPage)
3640
+ };
3641
+ }
3642
+
3643
+ // BUG: doesn't handle negative numbers
3644
+ function formatPrice(cents) {
3645
+ return '$' + (cents / 100).toFixed(2);
3646
+ }
3647
+
3648
+ module.exports = { paginate, formatPrice };
3649
+ UTILSJS
3650
+
3651
+ # --- 2. Git init ---
3652
+ cd "$demo_dir"
3653
+ git init --quiet
3654
+ echo "node_modules/" > .gitignore
3655
+ echo ".env" >> .gitignore
3656
+ git add -A
3657
+ git commit --quiet -m "Initial commit: Express.js demo app"
3658
+ cd - > /dev/null
3659
+
3660
+ # --- 3. Install CortexHawk ---
3661
+ echo " Installing CortexHawk..."
3662
+ (cd "$demo_dir" && bash "$SCRIPT_DIR/install.sh" --no-scan --profile fullstack 2>&1 | grep -E "^(CortexHawk| [0-9])" || true)
3663
+
3664
+ echo ""
3665
+ echo " Demo project ready!"
3666
+ echo ""
3667
+ echo " ┌─────────────────────────────────────────────────────┐"
3668
+ echo " │ cd $demo_dir"
3669
+ echo " │ claude │"
3670
+ echo " │ │"
3671
+ echo " │ Then try: │"
3672
+ echo " │ /scan — find the SQL injection + vulns │"
3673
+ echo " │ /debug — find the off-by-one bug │"
3674
+ echo " │ /check — pre-commit quality gate │"
3675
+ echo " │ /test — generate missing tests │"
3676
+ echo " │ /pulse — project health dashboard │"
3677
+ echo " │ /plan add auth — plan an auth system │"
3678
+ echo " │ │"
3679
+ echo " │ The project has intentional bugs and vulns │"
3680
+ echo " │ for you to discover with CortexHawk agents. │"
3681
+ echo " └─────────────────────────────────────────────────────┘"
3682
+ echo ""
3683
+ echo " Cleanup when done: rm -rf $demo_dir"
3684
+ echo ""
3685
+ }
3686
+
3687
+ # --- Dispatcher ---
3688
+ if [ "$DEMO_MODE" = true ]; then
3689
+ do_demo
3690
+ exit 0
3691
+ elif [ "$CHECK_UPDATE_MODE" = true ]; then
3692
+ do_check_update
3693
+ elif [ -n "$PUBLISH_SKILL_PATH" ]; then
3694
+ do_publish_skill "$PUBLISH_SKILL_PATH"
3695
+ exit 0
3696
+ elif [ "$STATS_MODE" = true ]; then
3697
+ do_stats
3698
+ exit 0
3699
+ elif [ "$QUICKSTART_MODE" = true ]; then
3700
+ do_quickstart
3701
+ exit 0
3702
+ elif [ "$TEST_HOOKS_MODE" = true ]; then
3703
+ do_test_hooks
3704
+ elif [ "$UNINSTALL_MODE" = true ]; then
3705
+ do_uninstall
3706
+ elif [ "$DOCTOR_MODE" = true ]; then
3707
+ do_doctor
3708
+ elif [ "$SNAPSHOTS_LIST" = true ]; then
3709
+ do_snapshots_list
3710
+ elif [ "$EXPORT_TEAM" = true ]; then
3711
+ do_export_team
3712
+ elif [ "$DIFF_MODE" = true ]; then
3713
+ if [ -n "$DIFF_FILE2" ]; then
3714
+ do_diff_semantic "$DIFF_FILE" "$DIFF_FILE2"
3715
+ else
3716
+ do_diff
3717
+ fi
3718
+ elif [ "$SNAPSHOT_MODE" = true ]; then
3719
+ do_snapshot
3720
+ elif [ "$RESTORE_MODE" = true ]; then
3721
+ do_restore "$SNAPSHOT_FILE"
3722
+ elif [ "$LIST_HOOKS" = true ]; then
3723
+ do_list_hooks
3724
+ elif [ -n "$ENABLE_HOOK" ]; then
3725
+ do_toggle_hook "$ENABLE_HOOK" "enable"
3726
+ elif [ -n "$DISABLE_HOOK" ]; then
3727
+ do_toggle_hook "$DISABLE_HOOK" "disable"
3728
+ elif [ -n "$SEARCH_KEYWORD" ]; then
3729
+ do_search_skills "$SEARCH_KEYWORD"
3730
+ elif [ -n "$ADD_SKILL_URL" ]; then
3731
+ do_add_skill "$ADD_SKILL_URL"
3732
+ elif [ "$TEAM_MODE" = true ]; then
3733
+ do_team_install
3734
+ elif [ "$UPDATE_MODE" = true ]; then
3735
+ do_update
3736
+ else
3737
+ echo "CortexHawk Installer"
3738
+ echo "====================="
3739
+
3740
+ case "$TARGET_CLI" in
3741
+ claude)
3742
+ install_claude
3743
+ ;;
3744
+ kimi)
3745
+ install_kimi
3746
+ ;;
3747
+ codex)
3748
+ source "$SCRIPT_DIR/scripts/install-codex.sh"
3749
+ install_codex
3750
+ ;;
3751
+ auto)
3752
+ local detected
3753
+ detected=$(detect_installed_clis)
3754
+ if [ -z "$detected" ]; then
3755
+ echo "Error: no supported CLI found (claude, kimi, codex)"
3756
+ echo "Install at least one CLI tool, or use --target claude"
3757
+ exit 1
3758
+ fi
3759
+ echo "Auto-detected CLIs: $detected"
3760
+ echo ""
3761
+ local auto_count=0
3762
+ for cli in $detected; do
3763
+ [ $auto_count -gt 0 ] && echo "" && echo "---" && echo ""
3764
+ case "$cli" in
3765
+ claude) install_claude ;;
3766
+ kimi) install_kimi ;;
3767
+ codex) source "$SCRIPT_DIR/scripts/install-codex.sh"; install_codex ;;
3768
+ esac
3769
+ auto_count=$((auto_count + 1))
3770
+ done
3771
+ echo ""
3772
+ echo "============================="
3773
+ echo "Installed for $auto_count target(s): $detected"
3774
+ echo " To activate: exit your CLI(s) and relaunch in this directory."
3775
+ ;;
3776
+ all)
3777
+ echo "Installing for all supported CLIs..."
3778
+ echo ""
3779
+ install_claude
3780
+ echo ""
3781
+ echo "---"
3782
+ echo ""
3783
+ install_kimi
3784
+ echo ""
3785
+ echo "---"
3786
+ echo ""
3787
+ source "$SCRIPT_DIR/scripts/install-codex.sh"
3788
+ install_codex
3789
+ echo ""
3790
+ echo "============================="
3791
+ echo "All targets installed: claude, kimi, codex"
3792
+ echo " To activate: exit your CLI(s) (ctrl+c) and relaunch in this directory."
3793
+ ;;
3794
+ *)
3795
+ echo "Error: unknown target '$TARGET_CLI'"
3796
+ echo "Supported targets: claude, kimi, codex, auto, all"
3797
+ exit 1
3798
+ ;;
3799
+ esac
3800
+ fi
3801
+
3802
+ # --- Cleanup temp files ---
3803
+ if [ -n "$PROFILE_FILE" ] && [[ "$PROFILE_FILE" == /tmp/cortexhawk-* ]]; then
3804
+ rm -f "$PROFILE_FILE" 2>/dev/null || true
3805
+ fi