@juho0719/cckit 0.2.1 → 0.2.4
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/assets/hooks/agent-track.sh +53 -0
- package/assets/hooks/auto-commit-push.sh +123 -0
- package/assets/hooks/on-prompt-start.sh +6 -0
- package/assets/hooks/post-edit-format.js +0 -1
- package/assets/hooks/post-edit-typecheck.js +0 -2
- package/assets/hooks/skill-track.sh +26 -0
- package/assets/hooks/subagent-notify.sh +20 -0
- package/assets/statusline/statusline.sh +186 -0
- package/dist/{chunk-OLLOS3GG.js → chunk-E3INXQNO.js} +38 -8
- package/dist/chunk-ID643AV4.js +37 -0
- package/dist/{chunk-3UNN3IBE.js → chunk-W7RWPDBH.js} +13 -3
- package/dist/{cli-D2Q5QUO7.js → cli-KZYBSIXO.js} +45 -17
- package/dist/{hooks-IIG2XK4I.js → hooks-A2WQ2LGG.js} +1 -1
- package/dist/index.js +12 -13
- package/dist/{registry-BU55RMHU.js → registry-KRLOB4TH.js} +1 -1
- package/dist/rules-EFSJ3L3A.js +9 -0
- package/dist/{uninstall-cli-VCOZGDBM.js → uninstall-cli-GLYJG5V2.js} +44 -89
- package/package.json +1 -1
- package/dist/chunk-RMUKD7CW.js +0 -44
- package/dist/rules-2CPBVNNJ.js +0 -7
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Agent 툴 실행 추적 — 여러 에이전트 동시 지원
|
|
3
|
+
# @hook-event PreToolUse|Agent|pre
|
|
4
|
+
# @hook-event PostToolUse|Agent|post
|
|
5
|
+
|
|
6
|
+
AGENT_STATE_DIR="/tmp/claude-agents"
|
|
7
|
+
mkdir -p "$AGENT_STATE_DIR"
|
|
8
|
+
|
|
9
|
+
EVENT="${1:-post}"
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
|
|
12
|
+
# subagent_type + description으로 고유 파일명 생성
|
|
13
|
+
AGENT_ID=$(echo "$INPUT" | python3 -c "
|
|
14
|
+
import sys, json, hashlib
|
|
15
|
+
try:
|
|
16
|
+
d = json.load(sys.stdin)
|
|
17
|
+
ti = d.get('tool_input', {})
|
|
18
|
+
key = (ti.get('subagent_type', '') or '') + '|' + (ti.get('description', '') or '')
|
|
19
|
+
print(hashlib.md5(key.encode()).hexdigest()[:12])
|
|
20
|
+
except:
|
|
21
|
+
print('')
|
|
22
|
+
" 2>/dev/null)
|
|
23
|
+
|
|
24
|
+
if [ -z "$AGENT_ID" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ "$EVENT" = "pre" ]; then
|
|
29
|
+
SUBAGENT_TYPE=$(echo "$INPUT" | python3 -c "
|
|
30
|
+
import sys, json
|
|
31
|
+
try:
|
|
32
|
+
d = json.load(sys.stdin)
|
|
33
|
+
print(d.get('tool_input', {}).get('subagent_type', '') or '')
|
|
34
|
+
except:
|
|
35
|
+
print('')
|
|
36
|
+
" 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
DESCRIPTION=$(echo "$INPUT" | python3 -c "
|
|
39
|
+
import sys, json
|
|
40
|
+
try:
|
|
41
|
+
d = json.load(sys.stdin)
|
|
42
|
+
desc = d.get('tool_input', {}).get('description', '') or ''
|
|
43
|
+
print(desc[:50] if len(desc) > 50 else desc)
|
|
44
|
+
except:
|
|
45
|
+
print('')
|
|
46
|
+
" 2>/dev/null)
|
|
47
|
+
|
|
48
|
+
if [ -n "$SUBAGENT_TYPE" ]; then
|
|
49
|
+
echo "${SUBAGENT_TYPE}|${DESCRIPTION}" > "${AGENT_STATE_DIR}/${AGENT_ID}"
|
|
50
|
+
fi
|
|
51
|
+
else
|
|
52
|
+
rm -f "${AGENT_STATE_DIR}/${AGENT_ID}"
|
|
53
|
+
fi
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Auto commit & push hook - runs when Claude Code finishes a response
|
|
3
|
+
# @hook-event Stop||
|
|
4
|
+
# Compatible with bash 3.2 (macOS default)
|
|
5
|
+
|
|
6
|
+
# Get the working directory from the hook input
|
|
7
|
+
WORK_DIR=$(echo "$CLAUDE_HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
8
|
+
if [ -z "$WORK_DIR" ]; then
|
|
9
|
+
WORK_DIR="$(pwd)"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
cd "$WORK_DIR" 2>/dev/null || exit 0
|
|
13
|
+
|
|
14
|
+
# Check if we're in a git repo
|
|
15
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Check if there are any changes
|
|
20
|
+
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
25
|
+
if [ -z "$BRANCH" ]; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Stage all changes
|
|
30
|
+
git add -A
|
|
31
|
+
|
|
32
|
+
# ── 변경 파일 목록 수집 ───────────────────────────────
|
|
33
|
+
ADDED_FILES=$(git diff --cached --diff-filter=A --name-only)
|
|
34
|
+
MODIFIED_FILES=$(git diff --cached --diff-filter=M --name-only)
|
|
35
|
+
DELETED_FILES=$(git diff --cached --diff-filter=D --name-only)
|
|
36
|
+
|
|
37
|
+
ALL_FILES=$(git diff --cached --name-only)
|
|
38
|
+
ADDED_COUNT=$(echo "$ADDED_FILES" | grep -c . || true)
|
|
39
|
+
MODIFIED_COUNT=$(echo "$MODIFIED_FILES" | grep -c . || true)
|
|
40
|
+
DELETED_COUNT=$(echo "$DELETED_FILES" | grep -c . || true)
|
|
41
|
+
|
|
42
|
+
# ── 커밋 타입 감지 ────────────────────────────────────
|
|
43
|
+
# test: 파일이 모두 테스트/설정 관련이면 test/chore
|
|
44
|
+
HAS_TEST=$(echo "$ALL_FILES" | grep -E "(\.test\.|\.spec\.|__tests__|e2e/)" | head -1)
|
|
45
|
+
HAS_CONFIG=$(echo "$ALL_FILES" | grep -E "\.(config|json|yaml|yml|env|toml)$" | head -1)
|
|
46
|
+
HAS_ROADMAP=$(echo "$ALL_FILES" | grep -E "roadmap|ROADMAP|\.md$" | head -1)
|
|
47
|
+
HAS_SRC=$(echo "$ALL_FILES" | grep -E "^src/" | head -1)
|
|
48
|
+
|
|
49
|
+
if [ -n "$HAS_TEST" ] && [ -z "$HAS_SRC" ]; then
|
|
50
|
+
COMMIT_TYPE="test"
|
|
51
|
+
elif [ -n "$HAS_CONFIG" ] && [ -z "$HAS_SRC" ]; then
|
|
52
|
+
COMMIT_TYPE="chore"
|
|
53
|
+
elif [ -n "$HAS_ROADMAP" ] && [ -z "$HAS_SRC" ]; then
|
|
54
|
+
COMMIT_TYPE="docs"
|
|
55
|
+
else
|
|
56
|
+
COMMIT_TYPE="feat"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ── 스코프: 가장 많이 변경된 2단계 디렉토리 ────────────
|
|
60
|
+
SCOPE=$(echo "$ALL_FILES" \
|
|
61
|
+
| awk -F'/' 'NF>=2 {print $1"/"$2} NF==1 {print $1}' \
|
|
62
|
+
| sort | uniq -c | sort -rn \
|
|
63
|
+
| awk 'NR==1{print $2}')
|
|
64
|
+
[ -z "$SCOPE" ] && SCOPE="root"
|
|
65
|
+
|
|
66
|
+
# ── Subject: 주요 파일명 나열 (확장자 제거, 최대 3개) ───
|
|
67
|
+
key_files() {
|
|
68
|
+
echo "$1" | while IFS= read -r f; do
|
|
69
|
+
[ -n "$f" ] && basename "$f" | sed 's/\.[^.]*$//'
|
|
70
|
+
done | head -3 | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ADDED_NAMES=$(key_files "$ADDED_FILES")
|
|
74
|
+
MODIFIED_NAMES=$(key_files "$MODIFIED_FILES")
|
|
75
|
+
DELETED_NAMES=$(key_files "$DELETED_FILES")
|
|
76
|
+
|
|
77
|
+
SUBJECT_PARTS=""
|
|
78
|
+
[ -n "$ADDED_NAMES" ] && SUBJECT_PARTS="${SUBJECT_PARTS}add ${ADDED_NAMES}"
|
|
79
|
+
[ -n "$MODIFIED_NAMES" ] && SUBJECT_PARTS="${SUBJECT_PARTS:+$SUBJECT_PARTS; }update ${MODIFIED_NAMES}"
|
|
80
|
+
[ -n "$DELETED_NAMES" ] && SUBJECT_PARTS="${SUBJECT_PARTS:+$SUBJECT_PARTS; }remove ${DELETED_NAMES}"
|
|
81
|
+
|
|
82
|
+
# 파일이 4개 이상이면 개수도 병기
|
|
83
|
+
TOTAL=$((ADDED_COUNT + MODIFIED_COUNT + DELETED_COUNT))
|
|
84
|
+
if [ "$TOTAL" -gt 3 ]; then
|
|
85
|
+
SUBJECT_PARTS="${SUBJECT_PARTS} (+$((TOTAL - 3)) more)"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
SUBJECT="${COMMIT_TYPE}(${SCOPE}): ${SUBJECT_PARTS}"
|
|
89
|
+
|
|
90
|
+
# ── Body: 전체 파일 목록 ─────────────────────────────
|
|
91
|
+
BODY=""
|
|
92
|
+
if [ -n "$ADDED_FILES" ]; then
|
|
93
|
+
BODY="${BODY}New files:\n"
|
|
94
|
+
while IFS= read -r f; do
|
|
95
|
+
[ -n "$f" ] && BODY="${BODY} + ${f}\n"
|
|
96
|
+
done <<< "$ADDED_FILES"
|
|
97
|
+
fi
|
|
98
|
+
if [ -n "$MODIFIED_FILES" ]; then
|
|
99
|
+
[ -n "$BODY" ] && BODY="${BODY}\n"
|
|
100
|
+
BODY="${BODY}Modified:\n"
|
|
101
|
+
while IFS= read -r f; do
|
|
102
|
+
[ -n "$f" ] && BODY="${BODY} ~ ${f}\n"
|
|
103
|
+
done <<< "$MODIFIED_FILES"
|
|
104
|
+
fi
|
|
105
|
+
if [ -n "$DELETED_FILES" ]; then
|
|
106
|
+
[ -n "$BODY" ] && BODY="${BODY}\n"
|
|
107
|
+
BODY="${BODY}Deleted:\n"
|
|
108
|
+
while IFS= read -r f; do
|
|
109
|
+
[ -n "$f" ] && BODY="${BODY} - ${f}\n"
|
|
110
|
+
done <<< "$DELETED_FILES"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
FULL_MSG="${SUBJECT}\n\n${BODY}"
|
|
114
|
+
|
|
115
|
+
git commit -m "$(printf "%b" "$FULL_MSG")" --no-verify 2>/dev/null
|
|
116
|
+
|
|
117
|
+
# Push to remote if available
|
|
118
|
+
REMOTE=$(git remote 2>/dev/null | head -1)
|
|
119
|
+
if [ -n "$REMOTE" ]; then
|
|
120
|
+
git push "$REMOTE" "$BRANCH" --no-verify 2>/dev/null
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
exit 0
|
|
@@ -32,7 +32,6 @@ process.stdin.on("end", () => {
|
|
|
32
32
|
if (filePath && /\.(ts|tsx)$/.test(filePath)) {
|
|
33
33
|
const resolvedPath = path.resolve(filePath);
|
|
34
34
|
if (!fs.existsSync(resolvedPath)) {
|
|
35
|
-
process.stdout.write(data);
|
|
36
35
|
process.exit(0);
|
|
37
36
|
}
|
|
38
37
|
// Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
|
|
@@ -91,6 +90,5 @@ process.stdin.on("end", () => {
|
|
|
91
90
|
// Invalid input — pass through
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
process.stdout.write(data);
|
|
95
93
|
process.exit(0);
|
|
96
94
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Skill 툴 실행 추적
|
|
3
|
+
# @hook-event PreToolUse|Skill|pre
|
|
4
|
+
# @hook-event PostToolUse|Skill|post
|
|
5
|
+
|
|
6
|
+
SKILL_STATE_FILE="/tmp/claude-active-skill.txt"
|
|
7
|
+
|
|
8
|
+
EVENT="${1:-post}"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
if [ "$EVENT" = "pre" ]; then
|
|
12
|
+
SKILL_NAME=$(echo "$INPUT" | python3 -c "
|
|
13
|
+
import sys, json
|
|
14
|
+
try:
|
|
15
|
+
d = json.load(sys.stdin)
|
|
16
|
+
print(d.get('tool_input', {}).get('skill', '') or '')
|
|
17
|
+
except:
|
|
18
|
+
print('')
|
|
19
|
+
" 2>/dev/null)
|
|
20
|
+
|
|
21
|
+
if [ -n "$SKILL_NAME" ]; then
|
|
22
|
+
echo "$SKILL_NAME" > "$SKILL_STATE_FILE"
|
|
23
|
+
fi
|
|
24
|
+
else
|
|
25
|
+
rm -f "$SKILL_STATE_FILE"
|
|
26
|
+
fi
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 응답 완료 시 macOS 알림 발송 — 마지막 커밋 메시지를 요약으로 표시
|
|
3
|
+
# @hook-event Stop||
|
|
4
|
+
|
|
5
|
+
WORK_DIR=$(echo "$CLAUDE_HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
6
|
+
if [ -z "$WORK_DIR" ]; then
|
|
7
|
+
WORK_DIR="$(pwd)"
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# 마지막 git 커밋 메시지를 알림 본문으로 사용
|
|
11
|
+
MSG=""
|
|
12
|
+
if git -C "$WORK_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
|
|
13
|
+
MSG=$(git -C "$WORK_DIR" log --oneline -1 --format="%s" 2>/dev/null)
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
if [ -z "$MSG" ]; then
|
|
17
|
+
MSG="응답이 완료됐습니다."
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
osascript -e "display notification \"${MSG}\" with title \"Claude Code\" subtitle \"응답 완료\" sound name \"Glass\""
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Docs:
|
|
3
|
+
# - https://code.claude.com/docs/en/statusline
|
|
4
|
+
# - https://code.claude.com/docs/en/settings
|
|
5
|
+
#
|
|
6
|
+
# Notes:
|
|
7
|
+
# - Claude Code reruns the statusline when conversation state changes, debounced at about 300ms.
|
|
8
|
+
# - `agent.name` is officially provided.
|
|
9
|
+
# - Active skills are not officially exposed in the statusline JSON, so `skills` below is a best-effort
|
|
10
|
+
# inference from recent `/skill-name` or `$skill-name` mentions in the transcript.
|
|
11
|
+
|
|
12
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
13
|
+
printf '[--] | 📁 -- | default\n'
|
|
14
|
+
printf '◉ CTX unavailable | 🤖 main | 🧩 --\n'
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
input=$(cat 2>/dev/null)
|
|
19
|
+
[ -n "$input" ] || exit 0
|
|
20
|
+
|
|
21
|
+
repeat_char() {
|
|
22
|
+
local count="$1"
|
|
23
|
+
local char="$2"
|
|
24
|
+
local out=""
|
|
25
|
+
while [ "$count" -gt 0 ]; do
|
|
26
|
+
out="${out}${char}"
|
|
27
|
+
count=$((count - 1))
|
|
28
|
+
done
|
|
29
|
+
printf '%s' "$out"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
shorten() {
|
|
33
|
+
local text="$1"
|
|
34
|
+
local max="${2:-24}"
|
|
35
|
+
if [ "${#text}" -le "$max" ]; then
|
|
36
|
+
printf '%s' "$text"
|
|
37
|
+
else
|
|
38
|
+
printf '%s…' "${text:0:max-1}"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current_dir_name() {
|
|
43
|
+
local path="$1"
|
|
44
|
+
path="${path%/}"
|
|
45
|
+
|
|
46
|
+
if [ -z "$path" ]; then
|
|
47
|
+
printf '/'
|
|
48
|
+
elif [ "$path" = "$HOME" ]; then
|
|
49
|
+
printf '~'
|
|
50
|
+
else
|
|
51
|
+
printf '%s' "${path##*/}"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
current_effort() {
|
|
56
|
+
if [ "${MAX_THINKING_TOKENS:-}" = "0" ]; then
|
|
57
|
+
printf 'off'
|
|
58
|
+
elif [ -n "${CLAUDE_CODE_EFFORT_LEVEL:-}" ]; then
|
|
59
|
+
printf '%s' "$CLAUDE_CODE_EFFORT_LEVEL"
|
|
60
|
+
elif [ "${CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING:-}" = "1" ] && [ -n "${MAX_THINKING_TOKENS:-}" ]; then
|
|
61
|
+
printf 'think:%s' "$MAX_THINKING_TOKENS"
|
|
62
|
+
elif [ "${CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING:-}" = "1" ]; then
|
|
63
|
+
printf 'fixed'
|
|
64
|
+
else
|
|
65
|
+
printf 'default'
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
infer_skills() {
|
|
70
|
+
local transcript="$1"
|
|
71
|
+
local skills
|
|
72
|
+
|
|
73
|
+
if [ -z "$transcript" ] || [ ! -f "$transcript" ]; then
|
|
74
|
+
printf '%s' '--'
|
|
75
|
+
return
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
skills=$(
|
|
79
|
+
tail -n 200 "$transcript" 2>/dev/null |
|
|
80
|
+
jq -r '.. | .text? // empty' 2>/dev/null |
|
|
81
|
+
grep -Eo '(^|[[:space:]])(\$|/)[A-Za-z][A-Za-z0-9_-]+' |
|
|
82
|
+
sed -E 's/^[[:space:]]+//; s#^[/$]##' |
|
|
83
|
+
grep -Evi '^(add-dir|agents|bug|clear|compact|config|cost|doctor|help|init|login|logout|mcp|memory|model|permissions|plugin|plugins|pr-comments|review|status|statusline|terminal-setup|vim)$' |
|
|
84
|
+
awk '!seen[$0]++' |
|
|
85
|
+
head -n 3 |
|
|
86
|
+
awk 'BEGIN{ORS=""} {if (NR>1) printf ", "; printf $0}'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
printf '%s' "${skills:---}"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx_color() {
|
|
93
|
+
local pct="$1"
|
|
94
|
+
if [ "$pct" -ge 85 ]; then
|
|
95
|
+
printf '\033[38;2;255;107;107m'
|
|
96
|
+
elif [ "$pct" -ge 60 ]; then
|
|
97
|
+
printf '\033[38;2;255;184;77m'
|
|
98
|
+
else
|
|
99
|
+
printf '\033[38;2;92;224;163m'
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
IFS=$'\t' read -r model dir agent transcript ctx_size cur_in cur_cache_create cur_cache_read used_fallback worktree_branch < <(
|
|
104
|
+
printf '%s' "$input" | jq -r '[
|
|
105
|
+
.model.display_name // .model.id // "--",
|
|
106
|
+
.workspace.current_dir // .cwd // "",
|
|
107
|
+
.agent.name // "main",
|
|
108
|
+
.transcript_path // "",
|
|
109
|
+
(.context_window.context_window_size // 0),
|
|
110
|
+
(.context_window.current_usage.input_tokens? // 0),
|
|
111
|
+
(.context_window.current_usage.cache_creation_input_tokens? // 0),
|
|
112
|
+
(.context_window.current_usage.cache_read_input_tokens? // 0),
|
|
113
|
+
(.context_window.used_percentage // 0 | floor),
|
|
114
|
+
.worktree.branch // ""
|
|
115
|
+
] | @tsv'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
dir="${dir:-$PWD}"
|
|
119
|
+
model="${model:---}"
|
|
120
|
+
agent="${agent:-main}"
|
|
121
|
+
|
|
122
|
+
used_tokens=$((cur_in + cur_cache_create + cur_cache_read))
|
|
123
|
+
if [ "$ctx_size" -gt 0 ] && [ "$used_tokens" -gt 0 ]; then
|
|
124
|
+
used_pct=$((used_tokens * 100 / ctx_size))
|
|
125
|
+
else
|
|
126
|
+
used_pct="${used_fallback:-0}"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
case "$used_pct" in
|
|
130
|
+
''|*[!0-9]*)
|
|
131
|
+
used_pct=0
|
|
132
|
+
;;
|
|
133
|
+
esac
|
|
134
|
+
|
|
135
|
+
[ "$used_pct" -lt 0 ] && used_pct=0
|
|
136
|
+
[ "$used_pct" -gt 100 ] && used_pct=100
|
|
137
|
+
|
|
138
|
+
branch="$worktree_branch"
|
|
139
|
+
if [ -z "$branch" ] && [ -n "$dir" ] && git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
140
|
+
branch="$(git -C "$dir" symbolic-ref --quiet --short HEAD 2>/dev/null || git -C "$dir" rev-parse --short HEAD 2>/dev/null)"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
skills="$(infer_skills "$transcript")"
|
|
144
|
+
effort="$(current_effort)"
|
|
145
|
+
|
|
146
|
+
dir_label="$(shorten "$(current_dir_name "$dir")" 24)"
|
|
147
|
+
branch_label="$(shorten "$branch" 26)"
|
|
148
|
+
agent_label="$(shorten "$agent" 18)"
|
|
149
|
+
skills_label="$(shorten "$skills" 32)"
|
|
150
|
+
|
|
151
|
+
bar_width=20
|
|
152
|
+
filled=$((used_pct * bar_width / 100))
|
|
153
|
+
empty=$((bar_width - filled))
|
|
154
|
+
|
|
155
|
+
RESET=$'\033[0m'
|
|
156
|
+
BOLD=$'\033[1m'
|
|
157
|
+
DIM=$'\033[2m'
|
|
158
|
+
|
|
159
|
+
MODEL_C=$'\033[1;38;2;110;200;255m'
|
|
160
|
+
DIR_C=$'\033[38;2;142;180;255m'
|
|
161
|
+
BRANCH_C=$'\033[38;2;120;230;160m'
|
|
162
|
+
EFFORT_C=$'\033[38;2;255;196;107m'
|
|
163
|
+
AGENT_C=$'\033[38;2;94;234;212m'
|
|
164
|
+
SKILL_C=$'\033[38;2;255;121;198m'
|
|
165
|
+
SEP_C=$'\033[38;2;108;114;134m'
|
|
166
|
+
FRAME_C=$'\033[38;2;96;103;122m'
|
|
167
|
+
TRACK_C=$'\033[38;2;62;68;84m'
|
|
168
|
+
LABEL_C=$'\033[38;2;145;215;255m'
|
|
169
|
+
PCT_C="$(ctx_color "$used_pct")"
|
|
170
|
+
|
|
171
|
+
filled_bar="$(repeat_char "$filled" '█')"
|
|
172
|
+
empty_bar="$(repeat_char "$empty" '░')"
|
|
173
|
+
|
|
174
|
+
line1="${MODEL_C}[${model}]${RESET} ${SEP_C}|${RESET} ${DIR_C}📁 ${dir_label}${RESET}"
|
|
175
|
+
if [ -n "$branch_label" ]; then
|
|
176
|
+
line1="${line1} ${SEP_C}|${RESET} ${BRANCH_C}⎇ ${branch_label}${RESET}"
|
|
177
|
+
fi
|
|
178
|
+
line1="${line1} ${SEP_C}|${RESET} ${EFFORT_C}${effort}${RESET}"
|
|
179
|
+
|
|
180
|
+
line2="${LABEL_C}◉ CTX ${FRAME_C}▕${PCT_C}${filled_bar}${TRACK_C}${empty_bar}${FRAME_C}▏ ${PCT_C}${used_pct}%${RESET}"
|
|
181
|
+
line2="${line2} ${SEP_C}|${RESET} ${AGENT_C}🤖 ${agent_label}${RESET}"
|
|
182
|
+
line2="${line2} ${SEP_C}|${RESET} ${SKILL_C}🧩 ${skills_label}${RESET}"
|
|
183
|
+
|
|
184
|
+
printf '%b\n' "$line1"
|
|
185
|
+
printf '%b\n' "$line2"
|
|
186
|
+
|
|
@@ -72,17 +72,47 @@ async function getCommands() {
|
|
|
72
72
|
}
|
|
73
73
|
async function getHooks() {
|
|
74
74
|
const dir = join(getAssetsDir(), "hooks");
|
|
75
|
-
const
|
|
75
|
+
const allFiles = await readdir(dir);
|
|
76
|
+
const files = allFiles.filter((f) => f.endsWith(".js") || f.endsWith(".sh"));
|
|
76
77
|
const items = [];
|
|
77
78
|
for (const file of files) {
|
|
78
79
|
const content = await readFile(join(dir, file), "utf-8");
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
if (file.endsWith(".js")) {
|
|
81
|
+
const descMatch = content.match(/\*\s+PostToolUse Hook:\s*(.+)/);
|
|
82
|
+
const description = descMatch ? descMatch[1].trim() : file;
|
|
83
|
+
const hookType = "PostToolUse";
|
|
84
|
+
let matcher = "Edit";
|
|
85
|
+
if (file.includes("typecheck")) matcher = "Edit|Write";
|
|
86
|
+
if (file.includes("format")) matcher = "Edit|Write";
|
|
87
|
+
items.push({ name: file, description, file, hookType, matcher });
|
|
88
|
+
} else {
|
|
89
|
+
const lines = content.split("\n");
|
|
90
|
+
const descLine = lines.find(
|
|
91
|
+
(l) => l.startsWith("#") && !l.startsWith("#!") && !l.includes("@hook-event")
|
|
92
|
+
);
|
|
93
|
+
const description = descLine ? descLine.replace(/^#\s*/, "") : file;
|
|
94
|
+
const eventRegex = /^# @hook-event ([^|]+)\|([^|]*)\|?(.*)$/gm;
|
|
95
|
+
const registrations = [];
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = eventRegex.exec(content)) !== null) {
|
|
98
|
+
registrations.push({
|
|
99
|
+
hookType: match[1].trim(),
|
|
100
|
+
matcher: match[2].trim(),
|
|
101
|
+
args: match[3].trim() || void 0
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (registrations.length === 0) continue;
|
|
105
|
+
const [first] = registrations;
|
|
106
|
+
items.push({
|
|
107
|
+
name: file,
|
|
108
|
+
description,
|
|
109
|
+
file,
|
|
110
|
+
hookType: first.hookType,
|
|
111
|
+
matcher: first.matcher,
|
|
112
|
+
args: first.args,
|
|
113
|
+
registrations: registrations.length > 1 ? registrations : void 0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
86
116
|
}
|
|
87
117
|
return items;
|
|
88
118
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
backupIfExists
|
|
3
|
+
} from "./chunk-K25UZZVG.js";
|
|
4
|
+
import {
|
|
5
|
+
copyFileUtil,
|
|
6
|
+
ensureDir
|
|
7
|
+
} from "./chunk-3GUKEMND.js";
|
|
8
|
+
import {
|
|
9
|
+
getAssetsDir
|
|
10
|
+
} from "./chunk-5XOKKPAA.js";
|
|
11
|
+
|
|
12
|
+
// src/installers/rules.ts
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { readdir } from "fs/promises";
|
|
15
|
+
async function installRules(categories, targetDir) {
|
|
16
|
+
const srcRulesDir = join(getAssetsDir(), "rules");
|
|
17
|
+
for (const category of categories) {
|
|
18
|
+
const srcCatDir = join(srcRulesDir, category);
|
|
19
|
+
let files;
|
|
20
|
+
try {
|
|
21
|
+
files = (await readdir(srcCatDir)).filter((f) => f.endsWith(".md"));
|
|
22
|
+
} catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const destCatDir = join(targetDir, "rules", category);
|
|
26
|
+
await ensureDir(destCatDir);
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const dest = join(destCatDir, file);
|
|
29
|
+
await backupIfExists(dest);
|
|
30
|
+
await copyFileUtil(join(srcCatDir, file), dest);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
installRules
|
|
37
|
+
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
// src/installers/hooks.ts
|
|
13
13
|
import { join } from "path";
|
|
14
|
-
import { readFile, writeFile, access } from "fs/promises";
|
|
14
|
+
import { readFile, writeFile, access, chmod } from "fs/promises";
|
|
15
15
|
import { constants } from "fs";
|
|
16
16
|
async function mergeHookSettings(settingsPath, hookType, matcher, command) {
|
|
17
17
|
let settings = {};
|
|
@@ -44,8 +44,18 @@ async function installHooks(hooks, targetDir) {
|
|
|
44
44
|
const dest = join(hooksDir, hook.file);
|
|
45
45
|
await backupIfExists(dest);
|
|
46
46
|
await copyFileUtil(join(srcDir, hook.file), dest);
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (hook.file.endsWith(".sh")) {
|
|
48
|
+
await chmod(dest, 493);
|
|
49
|
+
}
|
|
50
|
+
const runtime = hook.file.endsWith(".sh") ? "bash" : "node";
|
|
51
|
+
const regs = hook.registrations ?? [
|
|
52
|
+
{ hookType: hook.hookType, matcher: hook.matcher, args: hook.args }
|
|
53
|
+
];
|
|
54
|
+
for (const reg of regs) {
|
|
55
|
+
const argsStr = reg.args ? ` ${reg.args}` : "";
|
|
56
|
+
const hookCommand = `${runtime} "$CLAUDE_PROJECT_DIR"/.claude/hooks/${hook.file}${argsStr}`;
|
|
57
|
+
await mergeHookSettings(settingsPath, reg.hookType, reg.matcher, hookCommand);
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getMcpServers,
|
|
13
13
|
getRuleCategories,
|
|
14
14
|
getSkills
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-E3INXQNO.js";
|
|
16
16
|
import {
|
|
17
17
|
installAgents
|
|
18
18
|
} from "./chunk-EYY2IZ7N.js";
|
|
@@ -24,13 +24,18 @@ import {
|
|
|
24
24
|
} from "./chunk-3Y26YU4R.js";
|
|
25
25
|
import {
|
|
26
26
|
installHooks
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import "./chunk-K25UZZVG.js";
|
|
29
|
-
import "./chunk-3GUKEMND.js";
|
|
27
|
+
} from "./chunk-W7RWPDBH.js";
|
|
30
28
|
import {
|
|
31
29
|
installRules
|
|
32
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-ID643AV4.js";
|
|
33
31
|
import {
|
|
32
|
+
backupIfExists
|
|
33
|
+
} from "./chunk-K25UZZVG.js";
|
|
34
|
+
import {
|
|
35
|
+
copyFileUtil
|
|
36
|
+
} from "./chunk-3GUKEMND.js";
|
|
37
|
+
import {
|
|
38
|
+
getAssetsDir,
|
|
34
39
|
getGlobalDir,
|
|
35
40
|
getProjectDir
|
|
36
41
|
} from "./chunk-5XOKKPAA.js";
|
|
@@ -39,7 +44,20 @@ import {
|
|
|
39
44
|
import { checkbox, select, input, confirm } from "@inquirer/prompts";
|
|
40
45
|
import chalk from "chalk";
|
|
41
46
|
import ora from "ora";
|
|
47
|
+
import { join as join2 } from "path";
|
|
48
|
+
|
|
49
|
+
// src/installers/statusline.ts
|
|
42
50
|
import { join } from "path";
|
|
51
|
+
import { chmod } from "fs/promises";
|
|
52
|
+
async function installStatusline() {
|
|
53
|
+
const src = join(getAssetsDir(), "statusline", "statusline.sh");
|
|
54
|
+
const dest = join(getGlobalDir(), "statusline.sh");
|
|
55
|
+
await backupIfExists(dest);
|
|
56
|
+
await copyFileUtil(src, dest);
|
|
57
|
+
await chmod(dest, 493);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/cli.ts
|
|
43
61
|
function printBanner() {
|
|
44
62
|
console.log(
|
|
45
63
|
chalk.cyan.bold(`
|
|
@@ -79,7 +97,8 @@ async function runCli() {
|
|
|
79
97
|
{ name: "Hooks - Post-tool hooks", value: "hooks" },
|
|
80
98
|
{ name: "Rules - CLAUDE.md rules", value: "rules" },
|
|
81
99
|
{ name: "MCPs - MCP server configs", value: "mcps" },
|
|
82
|
-
{ name: "ClaudeMd
|
|
100
|
+
{ name: "ClaudeMd - Behavioral guidelines", value: "claudemd" },
|
|
101
|
+
{ name: "Statusline - Terminal statusline script", value: "statusline" }
|
|
83
102
|
]
|
|
84
103
|
});
|
|
85
104
|
if (selectedCategories.length === 0) {
|
|
@@ -95,7 +114,8 @@ async function runCli() {
|
|
|
95
114
|
ruleCategories: [],
|
|
96
115
|
mcps: [],
|
|
97
116
|
mcpEnvValues: /* @__PURE__ */ new Map(),
|
|
98
|
-
claudemds: []
|
|
117
|
+
claudemds: [],
|
|
118
|
+
statusline: false
|
|
99
119
|
};
|
|
100
120
|
const skippedMcps = /* @__PURE__ */ new Map();
|
|
101
121
|
if (selectedCategories.includes("agents")) {
|
|
@@ -164,6 +184,9 @@ async function runCli() {
|
|
|
164
184
|
}))
|
|
165
185
|
});
|
|
166
186
|
}
|
|
187
|
+
if (selectedCategories.includes("statusline")) {
|
|
188
|
+
plan.statusline = true;
|
|
189
|
+
}
|
|
167
190
|
if (selectedCategories.includes("mcps")) {
|
|
168
191
|
const allMcps = await getMcpServers();
|
|
169
192
|
plan.mcps = await checkbox({
|
|
@@ -213,9 +236,10 @@ async function runCli() {
|
|
|
213
236
|
if (plan.hooks.length) console.log(` Hooks : ${plan.hooks.map((h) => h.name).join(", ")}`);
|
|
214
237
|
if (plan.ruleCategories.length) console.log(` Rules : ${plan.ruleCategories.join(", ")}`);
|
|
215
238
|
if (plan.mcps.length) console.log(` MCPs : ${plan.mcps.map((m) => m.name).join(", ")}`);
|
|
216
|
-
if (plan.claudemds.length) console.log(` ClaudeMd: ${plan.claudemds.map((c) => c.name).join(", ")}`);
|
|
239
|
+
if (plan.claudemds.length) console.log(` ClaudeMd : ${plan.claudemds.map((c) => c.name).join(", ")}`);
|
|
240
|
+
if (plan.statusline) console.log(` Statusline: statusline.sh \u2192 ~/.claude/statusline.sh`);
|
|
217
241
|
console.log("");
|
|
218
|
-
const totalItems = plan.agents.length + plan.skills.length + plan.commands.length + plan.hooks.length + plan.ruleCategories.length + plan.mcps.length + plan.claudemds.length;
|
|
242
|
+
const totalItems = plan.agents.length + plan.skills.length + plan.commands.length + plan.hooks.length + plan.ruleCategories.length + plan.mcps.length + plan.claudemds.length + (plan.statusline ? 1 : 0);
|
|
219
243
|
if (totalItems === 0) {
|
|
220
244
|
console.log(chalk.yellow(" Nothing selected. Exiting."));
|
|
221
245
|
return;
|
|
@@ -246,8 +270,7 @@ async function runCli() {
|
|
|
246
270
|
}
|
|
247
271
|
if (plan.ruleCategories.length) {
|
|
248
272
|
spinner.text = "Installing rules...";
|
|
249
|
-
|
|
250
|
-
await installRules(plan.ruleCategories, claudeMd);
|
|
273
|
+
await installRules(plan.ruleCategories, targetDir);
|
|
251
274
|
}
|
|
252
275
|
if (plan.mcps.length) {
|
|
253
276
|
spinner.text = "Installing MCP servers...";
|
|
@@ -257,15 +280,20 @@ async function runCli() {
|
|
|
257
280
|
spinner.text = "Installing claudemd...";
|
|
258
281
|
await installClaudemd(plan.claudemds, targetDir);
|
|
259
282
|
}
|
|
283
|
+
if (plan.statusline) {
|
|
284
|
+
spinner.text = "Installing statusline...";
|
|
285
|
+
await installStatusline();
|
|
286
|
+
}
|
|
260
287
|
spinner.succeed(chalk.green("Installation complete!"));
|
|
261
288
|
console.log("");
|
|
262
|
-
if (plan.agents.length) console.log(` ${chalk.green("\u2713")} ${plan.agents.length} agent(s) \u2192 ${
|
|
263
|
-
if (plan.skills.length) console.log(` ${chalk.green("\u2713")} ${plan.skills.length} skill(s) \u2192 ${
|
|
264
|
-
if (plan.commands.length) console.log(` ${chalk.green("\u2713")} ${plan.commands.length} command(s) \u2192 ${
|
|
265
|
-
if (plan.hooks.length) console.log(` ${chalk.green("\u2713")} ${plan.hooks.length} hook(s) \u2192 ${
|
|
266
|
-
if (plan.ruleCategories.length) console.log(` ${chalk.green("\u2713")} Rules
|
|
289
|
+
if (plan.agents.length) console.log(` ${chalk.green("\u2713")} ${plan.agents.length} agent(s) \u2192 ${join2(targetDir, "agents")}`);
|
|
290
|
+
if (plan.skills.length) console.log(` ${chalk.green("\u2713")} ${plan.skills.length} skill(s) \u2192 ${join2(targetDir, "skills")}`);
|
|
291
|
+
if (plan.commands.length) console.log(` ${chalk.green("\u2713")} ${plan.commands.length} command(s) \u2192 ${join2(targetDir, "commands")}`);
|
|
292
|
+
if (plan.hooks.length) console.log(` ${chalk.green("\u2713")} ${plan.hooks.length} hook(s) \u2192 ${join2(targetDir, "hooks")}`);
|
|
293
|
+
if (plan.ruleCategories.length) console.log(` ${chalk.green("\u2713")} Rules copied \u2192 ${join2(targetDir, "rules")}`);
|
|
267
294
|
if (plan.mcps.length) console.log(` ${chalk.green("\u2713")} ${plan.mcps.length} MCP server(s) \u2192 ${scope === "global" ? "~/.claude.json" : "./.claude.json"}`);
|
|
268
|
-
if (plan.claudemds.length) console.log(` ${chalk.green("\u2713")} ClaudeMd appended \u2192 ${
|
|
295
|
+
if (plan.claudemds.length) console.log(` ${chalk.green("\u2713")} ClaudeMd appended \u2192 ${join2(targetDir, "CLAUDE.md")}`);
|
|
296
|
+
if (plan.statusline) console.log(` ${chalk.green("\u2713")} Statusline installed \u2192 ~/.claude/statusline.sh`);
|
|
269
297
|
console.log("");
|
|
270
298
|
if (skippedMcps.size > 0) {
|
|
271
299
|
console.log(chalk.yellow(" \u26A0 The following MCP servers have placeholder env vars:"));
|
package/dist/index.js
CHANGED
|
@@ -60,18 +60,18 @@ Installing all to ${targetDir}...
|
|
|
60
60
|
{ installMcps },
|
|
61
61
|
{ installClaudemd }
|
|
62
62
|
] = await Promise.all([
|
|
63
|
-
import("./registry-
|
|
64
|
-
import("./registry-
|
|
65
|
-
import("./registry-
|
|
66
|
-
import("./registry-
|
|
67
|
-
import("./registry-
|
|
68
|
-
import("./registry-
|
|
69
|
-
import("./registry-
|
|
63
|
+
import("./registry-KRLOB4TH.js"),
|
|
64
|
+
import("./registry-KRLOB4TH.js"),
|
|
65
|
+
import("./registry-KRLOB4TH.js"),
|
|
66
|
+
import("./registry-KRLOB4TH.js"),
|
|
67
|
+
import("./registry-KRLOB4TH.js"),
|
|
68
|
+
import("./registry-KRLOB4TH.js"),
|
|
69
|
+
import("./registry-KRLOB4TH.js"),
|
|
70
70
|
import("./agents-AEKT67A6.js"),
|
|
71
71
|
import("./skills-ULMW3UCM.js"),
|
|
72
72
|
import("./commands-P5LILVZ5.js"),
|
|
73
|
-
import("./hooks-
|
|
74
|
-
import("./rules-
|
|
73
|
+
import("./hooks-A2WQ2LGG.js"),
|
|
74
|
+
import("./rules-EFSJ3L3A.js"),
|
|
75
75
|
import("./mcps-67Q7TBGW.js"),
|
|
76
76
|
import("./claudemd-KKQ2DL7P.js")
|
|
77
77
|
]);
|
|
@@ -93,8 +93,7 @@ Installing all to ${targetDir}...
|
|
|
93
93
|
spinner.text = "Installing hooks...";
|
|
94
94
|
await installHooks(hooks, targetDir);
|
|
95
95
|
spinner.text = "Installing rules...";
|
|
96
|
-
|
|
97
|
-
await installRules(ruleCategories.map((rc) => rc.category), claudeMd);
|
|
96
|
+
await installRules(ruleCategories.map((rc) => rc.category), targetDir);
|
|
98
97
|
spinner.text = "Installing MCP servers...";
|
|
99
98
|
await installMcps(mcps, scope, /* @__PURE__ */ new Map());
|
|
100
99
|
spinner.text = "Installing claudemd...";
|
|
@@ -125,7 +124,7 @@ async function main() {
|
|
|
125
124
|
return;
|
|
126
125
|
}
|
|
127
126
|
if (args.includes("--uninstall") || args.includes("-u")) {
|
|
128
|
-
const { runUninstallCli } = await import("./uninstall-cli-
|
|
127
|
+
const { runUninstallCli } = await import("./uninstall-cli-GLYJG5V2.js");
|
|
129
128
|
await runUninstallCli();
|
|
130
129
|
return;
|
|
131
130
|
}
|
|
@@ -135,7 +134,7 @@ async function main() {
|
|
|
135
134
|
await installAll(scope);
|
|
136
135
|
return;
|
|
137
136
|
}
|
|
138
|
-
const { runCli } = await import("./cli-
|
|
137
|
+
const { runCli } = await import("./cli-KZYBSIXO.js");
|
|
139
138
|
await runCli();
|
|
140
139
|
}
|
|
141
140
|
main().catch((err) => {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getMcpServers,
|
|
7
7
|
getRuleCategories,
|
|
8
8
|
getSkills
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-E3INXQNO.js";
|
|
10
10
|
import {
|
|
11
11
|
getAssetsDir,
|
|
12
12
|
getGlobalDir,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
import { checkbox, select, confirm } from "@inquirer/prompts";
|
|
18
18
|
import chalk from "chalk";
|
|
19
19
|
import ora from "ora";
|
|
20
|
-
import { join as join9 } from "path";
|
|
21
20
|
|
|
22
21
|
// src/scanner.ts
|
|
23
22
|
import { readdir, readFile, access } from "fs/promises";
|
|
@@ -76,38 +75,17 @@ async function getInstalledHooks(targetDir) {
|
|
|
76
75
|
return registryHooks.filter((h) => installedFiles.includes(h.file));
|
|
77
76
|
}
|
|
78
77
|
async function getInstalledRuleCategories(targetDir) {
|
|
79
|
-
const
|
|
80
|
-
let
|
|
78
|
+
const rulesDir = join(targetDir, "rules");
|
|
79
|
+
let installedDirs = [];
|
|
81
80
|
try {
|
|
82
|
-
await
|
|
83
|
-
|
|
81
|
+
const entries = await readdir(rulesDir, { withFileTypes: true });
|
|
82
|
+
installedDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
84
83
|
} catch {
|
|
85
84
|
return [];
|
|
86
85
|
}
|
|
87
|
-
const installedHeaders = /* @__PURE__ */ new Set();
|
|
88
|
-
for (const line of content.split("\n")) {
|
|
89
|
-
const match = line.match(/^(#{1,2}\s+.+)/);
|
|
90
|
-
if (match) installedHeaders.add(match[1].trim());
|
|
91
|
-
}
|
|
92
86
|
const allCategories = await getRuleCategories();
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const catDir = join(getAssetsDir(), "rules", rc.category);
|
|
96
|
-
let found = false;
|
|
97
|
-
for (const file of rc.files) {
|
|
98
|
-
try {
|
|
99
|
-
const fileContent = await readFile(join(catDir, file), "utf-8");
|
|
100
|
-
const headerMatch = fileContent.match(/^(#{1,2}\s+.+)/m);
|
|
101
|
-
if (headerMatch && installedHeaders.has(headerMatch[1].trim())) {
|
|
102
|
-
found = true;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (found) installedCategories.push(rc.category);
|
|
109
|
-
}
|
|
110
|
-
return installedCategories;
|
|
87
|
+
const registryCategoryNames = new Set(allCategories.map((rc) => rc.category));
|
|
88
|
+
return installedDirs.filter((d) => registryCategoryNames.has(d));
|
|
111
89
|
}
|
|
112
90
|
async function getInstalledClaudemd(targetDir) {
|
|
113
91
|
const claudeMdPath = join(targetDir, "CLAUDE.md");
|
|
@@ -206,7 +184,7 @@ async function uninstallCommands(commands, targetDir) {
|
|
|
206
184
|
import { join as join5 } from "path";
|
|
207
185
|
import { rm as rm4, readFile as readFile2, writeFile, access as access5 } from "fs/promises";
|
|
208
186
|
import { constants as constants5 } from "fs";
|
|
209
|
-
async function removeHookFromSettings(settingsPath,
|
|
187
|
+
async function removeHookFromSettings(settingsPath, hookFileName) {
|
|
210
188
|
let settings = {};
|
|
211
189
|
try {
|
|
212
190
|
await access5(settingsPath, constants5.F_OK);
|
|
@@ -216,11 +194,12 @@ async function removeHookFromSettings(settingsPath, hookCommand) {
|
|
|
216
194
|
return;
|
|
217
195
|
}
|
|
218
196
|
if (!settings.hooks) return;
|
|
197
|
+
const suffix = `/hooks/${hookFileName}`;
|
|
219
198
|
for (const hookType of Object.keys(settings.hooks)) {
|
|
220
199
|
const matchers = settings.hooks[hookType];
|
|
221
200
|
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
222
201
|
const matcher = matchers[i];
|
|
223
|
-
matcher.hooks = matcher.hooks.filter((h) => h.command
|
|
202
|
+
matcher.hooks = matcher.hooks.filter((h) => !h.command.includes(suffix));
|
|
224
203
|
if (matcher.hooks.length === 0) {
|
|
225
204
|
matchers.splice(i, 1);
|
|
226
205
|
}
|
|
@@ -239,8 +218,7 @@ async function uninstallHooks(hooks, targetDir) {
|
|
|
239
218
|
const settingsPath = join5(targetDir, "settings.json");
|
|
240
219
|
for (const hook of hooks) {
|
|
241
220
|
const filePath = join5(hooksDir, hook.file);
|
|
242
|
-
|
|
243
|
-
await removeHookFromSettings(settingsPath, hookCommand);
|
|
221
|
+
await removeHookFromSettings(settingsPath, hook.file);
|
|
244
222
|
try {
|
|
245
223
|
await access5(filePath, constants5.F_OK);
|
|
246
224
|
await rm4(filePath);
|
|
@@ -250,69 +228,47 @@ async function uninstallHooks(hooks, targetDir) {
|
|
|
250
228
|
}
|
|
251
229
|
|
|
252
230
|
// src/uninstallers/rules.ts
|
|
253
|
-
import {
|
|
254
|
-
import { constants as constants6 } from "fs";
|
|
231
|
+
import { readdir as readdir2, rm as rm5 } from "fs/promises";
|
|
255
232
|
import { join as join6 } from "path";
|
|
256
|
-
async function uninstallRules(categories,
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
await access6(claudeMdPath, constants6.F_OK);
|
|
260
|
-
existing = await readFile3(claudeMdPath, "utf-8");
|
|
261
|
-
} catch {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const headersToRemove = /* @__PURE__ */ new Set();
|
|
233
|
+
async function uninstallRules(categories, targetDir) {
|
|
234
|
+
const assetsRulesDir = join6(getAssetsDir(), "rules");
|
|
265
235
|
for (const category of categories) {
|
|
266
|
-
const
|
|
267
|
-
let
|
|
236
|
+
const srcCatDir = join6(assetsRulesDir, category);
|
|
237
|
+
let assetFiles;
|
|
268
238
|
try {
|
|
269
|
-
|
|
270
|
-
files = (await readdir2(catDir)).filter((f) => f.endsWith(".md"));
|
|
239
|
+
assetFiles = (await readdir2(srcCatDir)).filter((f) => f.endsWith(".md"));
|
|
271
240
|
} catch {
|
|
272
241
|
continue;
|
|
273
242
|
}
|
|
274
|
-
|
|
243
|
+
const destCatDir = join6(targetDir, "rules", category);
|
|
244
|
+
for (const file of assetFiles) {
|
|
275
245
|
try {
|
|
276
|
-
|
|
277
|
-
const headerMatch = content.match(/^(#{1,2}\s+.+)/m);
|
|
278
|
-
if (headerMatch) {
|
|
279
|
-
headersToRemove.add(headerMatch[1].trim());
|
|
280
|
-
}
|
|
246
|
+
await rm5(join6(destCatDir, file));
|
|
281
247
|
} catch {
|
|
282
248
|
}
|
|
283
249
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let inRemovedSection = false;
|
|
289
|
-
let currentDepth = 0;
|
|
290
|
-
for (const line of lines) {
|
|
291
|
-
const headerMatch = line.match(/^(#{1,2})\s+/);
|
|
292
|
-
if (headerMatch) {
|
|
293
|
-
const depth = headerMatch[1].length;
|
|
294
|
-
const trimmed = line.trim();
|
|
295
|
-
if (headersToRemove.has(trimmed)) {
|
|
296
|
-
inRemovedSection = true;
|
|
297
|
-
currentDepth = depth;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
if (inRemovedSection && depth <= currentDepth) {
|
|
301
|
-
inRemovedSection = false;
|
|
250
|
+
try {
|
|
251
|
+
const remaining = await readdir2(destCatDir);
|
|
252
|
+
if (remaining.length === 0) {
|
|
253
|
+
await rm5(destCatDir, { recursive: true });
|
|
302
254
|
}
|
|
255
|
+
} catch {
|
|
303
256
|
}
|
|
304
|
-
|
|
305
|
-
|
|
257
|
+
}
|
|
258
|
+
const rulesDir = join6(targetDir, "rules");
|
|
259
|
+
try {
|
|
260
|
+
const remaining = await readdir2(rulesDir);
|
|
261
|
+
if (remaining.length === 0) {
|
|
262
|
+
await rm5(rulesDir, { recursive: true });
|
|
306
263
|
}
|
|
264
|
+
} catch {
|
|
307
265
|
}
|
|
308
|
-
const cleaned = result.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
309
|
-
await writeFile2(claudeMdPath, cleaned, "utf-8");
|
|
310
266
|
}
|
|
311
267
|
|
|
312
268
|
// src/uninstallers/mcps.ts
|
|
313
269
|
import { join as join7 } from "path";
|
|
314
|
-
import { readFile as
|
|
315
|
-
import { constants as
|
|
270
|
+
import { readFile as readFile3, writeFile as writeFile2, access as access6 } from "fs/promises";
|
|
271
|
+
import { constants as constants6 } from "fs";
|
|
316
272
|
import { homedir as homedir2 } from "os";
|
|
317
273
|
function getClaudeJsonPath2(scope) {
|
|
318
274
|
if (scope === "global") {
|
|
@@ -324,8 +280,8 @@ async function uninstallMcps(serverNames, scope) {
|
|
|
324
280
|
const filePath = getClaudeJsonPath2(scope);
|
|
325
281
|
let claudeJson = {};
|
|
326
282
|
try {
|
|
327
|
-
await
|
|
328
|
-
const content = await
|
|
283
|
+
await access6(filePath, constants6.F_OK);
|
|
284
|
+
const content = await readFile3(filePath, "utf-8");
|
|
329
285
|
claudeJson = JSON.parse(content);
|
|
330
286
|
} catch {
|
|
331
287
|
return;
|
|
@@ -334,26 +290,26 @@ async function uninstallMcps(serverNames, scope) {
|
|
|
334
290
|
for (const name of serverNames) {
|
|
335
291
|
delete claudeJson.mcpServers[name];
|
|
336
292
|
}
|
|
337
|
-
await
|
|
293
|
+
await writeFile2(filePath, JSON.stringify(claudeJson, null, 2) + "\n", "utf-8");
|
|
338
294
|
}
|
|
339
295
|
|
|
340
296
|
// src/uninstallers/claudemd.ts
|
|
341
|
-
import { readFile as
|
|
342
|
-
import { constants as
|
|
297
|
+
import { readFile as readFile4, writeFile as writeFile3, access as access7 } from "fs/promises";
|
|
298
|
+
import { constants as constants7 } from "fs";
|
|
343
299
|
import { join as join8 } from "path";
|
|
344
300
|
async function uninstallClaudemd(items, targetDir) {
|
|
345
301
|
const claudeMdPath = join8(targetDir, "CLAUDE.md");
|
|
346
302
|
let existing = "";
|
|
347
303
|
try {
|
|
348
|
-
await
|
|
349
|
-
existing = await
|
|
304
|
+
await access7(claudeMdPath, constants7.F_OK);
|
|
305
|
+
existing = await readFile4(claudeMdPath, "utf-8");
|
|
350
306
|
} catch {
|
|
351
307
|
return;
|
|
352
308
|
}
|
|
353
309
|
const headersToRemove = /* @__PURE__ */ new Set();
|
|
354
310
|
for (const item of items) {
|
|
355
311
|
try {
|
|
356
|
-
const raw = await
|
|
312
|
+
const raw = await readFile4(join8(getAssetsDir(), "claudemd", item.file), "utf-8");
|
|
357
313
|
const content = raw.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
|
|
358
314
|
const headerMatch = content.match(/^(#{1,2}\s+.+)/m);
|
|
359
315
|
if (headerMatch) {
|
|
@@ -386,7 +342,7 @@ async function uninstallClaudemd(items, targetDir) {
|
|
|
386
342
|
}
|
|
387
343
|
}
|
|
388
344
|
const cleaned = result.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
389
|
-
await
|
|
345
|
+
await writeFile3(claudeMdPath, cleaned, "utf-8");
|
|
390
346
|
}
|
|
391
347
|
|
|
392
348
|
// src/uninstall-cli.ts
|
|
@@ -613,8 +569,7 @@ async function runUninstallCli() {
|
|
|
613
569
|
}
|
|
614
570
|
if (plan.ruleCategories.length) {
|
|
615
571
|
spinner.text = "Removing rules...";
|
|
616
|
-
|
|
617
|
-
await uninstallRules(plan.ruleCategories, claudeMd);
|
|
572
|
+
await uninstallRules(plan.ruleCategories, targetDir);
|
|
618
573
|
}
|
|
619
574
|
if (plan.mcpNames.length) {
|
|
620
575
|
spinner.text = "Removing MCP servers...";
|
package/package.json
CHANGED
package/dist/chunk-RMUKD7CW.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getAssetsDir
|
|
3
|
-
} from "./chunk-5XOKKPAA.js";
|
|
4
|
-
|
|
5
|
-
// src/installers/rules.ts
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { readFile, writeFile, access } from "fs/promises";
|
|
8
|
-
import { constants } from "fs";
|
|
9
|
-
async function installRules(categories, targetClaudeMd) {
|
|
10
|
-
const rulesDir = join(getAssetsDir(), "rules");
|
|
11
|
-
let existing = "";
|
|
12
|
-
try {
|
|
13
|
-
await access(targetClaudeMd, constants.F_OK);
|
|
14
|
-
existing = await readFile(targetClaudeMd, "utf-8");
|
|
15
|
-
} catch {
|
|
16
|
-
}
|
|
17
|
-
const toAppend = [];
|
|
18
|
-
for (const category of categories) {
|
|
19
|
-
const catDir = join(rulesDir, category);
|
|
20
|
-
let files;
|
|
21
|
-
try {
|
|
22
|
-
const { readdir } = await import("fs/promises");
|
|
23
|
-
files = (await readdir(catDir)).filter((f) => f.endsWith(".md"));
|
|
24
|
-
} catch {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
const content = await readFile(join(catDir, file), "utf-8");
|
|
29
|
-
const headerMatch = content.match(/^#{1,2}\s+(.+)/m);
|
|
30
|
-
if (headerMatch) {
|
|
31
|
-
const header = headerMatch[0];
|
|
32
|
-
if (existing.includes(header)) continue;
|
|
33
|
-
}
|
|
34
|
-
toAppend.push(content.trim());
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (toAppend.length === 0) return;
|
|
38
|
-
const appendContent = (existing.endsWith("\n") ? "" : "\n") + toAppend.join("\n\n") + "\n";
|
|
39
|
-
await writeFile(targetClaudeMd, existing + appendContent, "utf-8");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
installRules
|
|
44
|
-
};
|