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.
- package/.cortexhawk-team.yml +65 -0
- package/CHANGELOG.md +268 -0
- package/CLAUDE.md +96 -0
- package/LICENSE +21 -0
- package/PACKS.md +14 -0
- package/README.md +418 -0
- package/REGISTRY.md +23 -0
- package/agents/architect.md +46 -0
- package/agents/brainstormer.md +57 -0
- package/agents/code-simplifier.md +56 -0
- package/agents/codebase-mapper.md +63 -0
- package/agents/copywriter.md +48 -0
- package/agents/debugger.md +44 -0
- package/agents/designer.md +53 -0
- package/agents/devops.md +49 -0
- package/agents/docs-manager.md +50 -0
- package/agents/fullstack-developer.md +55 -0
- package/agents/git-manager.md +63 -0
- package/agents/implementer.md +30 -0
- package/agents/journal-writer.md +53 -0
- package/agents/planner.md +52 -0
- package/agents/project-manager.md +50 -0
- package/agents/researcher.md +46 -0
- package/agents/reviewer.md +63 -0
- package/agents/security-auditor.md +92 -0
- package/agents/teacher.md +71 -0
- package/agents/tester.md +41 -0
- package/commands/api-gen.md +17 -0
- package/commands/backlog.md +26 -0
- package/commands/bootstrap.md +32 -0
- package/commands/brainstorm.md +18 -0
- package/commands/build.md +16 -0
- package/commands/chain.md +46 -0
- package/commands/changelog.md +16 -0
- package/commands/check.md +40 -0
- package/commands/ci.md +32 -0
- package/commands/context.md +35 -0
- package/commands/debug.md +16 -0
- package/commands/deploy.md +16 -0
- package/commands/doc.md +15 -0
- package/commands/export.md +17 -0
- package/commands/journal.md +18 -0
- package/commands/learn.md +16 -0
- package/commands/map.md +16 -0
- package/commands/migrate.md +17 -0
- package/commands/monitor.md +16 -0
- package/commands/optimize.md +17 -0
- package/commands/plan.md +17 -0
- package/commands/pulse.md +46 -0
- package/commands/refactor.md +16 -0
- package/commands/research.md +18 -0
- package/commands/review.md +16 -0
- package/commands/scan.md +19 -0
- package/commands/ship.md +17 -0
- package/commands/simplify.md +16 -0
- package/commands/task.md +32 -0
- package/commands/tdd.md +17 -0
- package/commands/test.md +16 -0
- package/commands/upgrade.md +27 -0
- package/cortexhawk +450 -0
- package/hooks/agent-analytics.sh +67 -0
- package/hooks/branch-guard.sh +56 -0
- package/hooks/codex-dispatcher.sh +84 -0
- package/hooks/commit-guard.sh +71 -0
- package/hooks/compose.yml +47 -0
- package/hooks/dependency-check.sh +56 -0
- package/hooks/file-guard.sh +69 -0
- package/hooks/hooks.json +46 -0
- package/hooks/self-review.sh +71 -0
- package/hooks/session-start.sh +132 -0
- package/hooks/session-telemetry.sh +60 -0
- package/hooks/test-reminder.sh +75 -0
- package/install.sh +3805 -0
- package/mcp/README.md +37 -0
- package/mcp/context7.json +8 -0
- package/mcp/puppeteer.json +8 -0
- package/mcp/sequential-thinking.json +8 -0
- package/modes/default.md +5 -0
- package/modes/fast.md +5 -0
- package/modes/learn.md +9 -0
- package/modes/orchestration.md +5 -0
- package/modes/pair.md +10 -0
- package/modes/research.md +5 -0
- package/modes/review.md +5 -0
- package/package.json +32 -0
- package/profiles/api.json +27 -0
- package/profiles/data.json +23 -0
- package/profiles/fullstack.json +27 -0
- package/scripts/autodetect-profile.sh +68 -0
- package/scripts/benchmark.sh +106 -0
- package/scripts/chain-post-save.sh +23 -0
- package/scripts/generate-plans-index.sh +50 -0
- package/scripts/git-workflow-init.sh +115 -0
- package/scripts/install-codex.sh +128 -0
- package/scripts/interactive-init.sh +264 -0
- package/scripts/post-install-audit.sh +130 -0
- package/scripts/validate.sh +214 -0
- package/settings.json +90 -0
- package/setup.sh +67 -0
- package/skills/databases/schema-designer/SKILL.md +54 -0
- package/skills/databases/sql-optimizer/SKILL.md +37 -0
- package/skills/devops/ci-cd/SKILL.md +59 -0
- package/skills/devops/deployment/SKILL.md +49 -0
- package/skills/devops/docker/SKILL.md +57 -0
- package/skills/frameworks/api-design/SKILL.md +103 -0
- package/skills/frameworks/fastapi/SKILL.md +68 -0
- package/skills/frameworks/nextjs/SKILL.md +74 -0
- package/skills/frameworks/python/SKILL.md +89 -0
- package/skills/frameworks/react/SKILL.md +83 -0
- package/skills/frameworks/sveltekit/SKILL.md +69 -0
- package/skills/frameworks/tailwindcss/SKILL.md +75 -0
- package/skills/frameworks/typescript/SKILL.md +94 -0
- package/skills/meta/mcp-builder/SKILL.md +54 -0
- package/skills/meta/skill-creator/SKILL.md +43 -0
- package/skills/optimization/performance/SKILL.md +70 -0
- package/skills/quality/complexity-analyzer/SKILL.md +52 -0
- package/skills/quality/error-handling/SKILL.md +123 -0
- package/skills/quality/log-analyzer/SKILL.md +31 -0
- package/skills/quality/pattern-detector/SKILL.md +50 -0
- package/skills/security/auth-analyzer/SKILL.md +96 -0
- package/skills/security/compliance-checker/SKILL.md +92 -0
- package/skills/security/container-security/SKILL.md +128 -0
- package/skills/security/dependency-auditor/SKILL.md +100 -0
- package/skills/security/encryption/SKILL.md +94 -0
- package/skills/security/incident-response/SKILL.md +127 -0
- package/skills/security/secrets/SKILL.md +93 -0
- package/skills/security/security-headers/SKILL.md +83 -0
- package/skills/security/security-logging/SKILL.md +107 -0
- package/skills/security/vulnerability-scanner/SKILL.md +114 -0
- package/skills/testing/e2e-testing/SKILL.md +119 -0
- package/skills/testing/tdd/SKILL.md +40 -0
- package/skills/testing/test-generator/SKILL.md +39 -0
- package/skills/workflow/commit/SKILL.md +61 -0
- package/skills/workflow/confidence-check/SKILL.md +90 -0
- package/skills/workflow/pr-review-comments/SKILL.md +81 -0
- 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
|