cc-discipline 2.12.1 → 2.12.3
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/init.sh +72 -2
- package/package.json +1 -1
- package/templates/.claude/hooks/git-guard.sh +26 -6
package/init.sh
CHANGED
|
@@ -377,14 +377,84 @@ echo -e "${GREEN}Installing skills...${NC}"
|
|
|
377
377
|
# v2.12.1. Symptom on an affected machine: a stray .claude/skills/SKILL.md plus
|
|
378
378
|
# skill dirs frozen at their first-install date. status/doctor could not catch
|
|
379
379
|
# it because they glob `skills/*/` and a bare file is not a directory.
|
|
380
|
+
# Skills are NOT force-overwritten. Users legitimately tune them per repo
|
|
381
|
+
# (/self-check's "Project-specific Checks" section exists precisely to be filled
|
|
382
|
+
# in), and until v2.12.2 every upgrade silently replaced those edits — a 73KB
|
|
383
|
+
# hand-written self-check was wiped twice in one day before this was fixed.
|
|
384
|
+
#
|
|
385
|
+
# The dpkg-conffile approach: remember the hash of the template WE installed. On
|
|
386
|
+
# the next upgrade, if the file on disk still matches that hash the user never
|
|
387
|
+
# touched it and we can update freely; if it differs the user owns it, so we keep
|
|
388
|
+
# their file and drop the new template beside it as SKILL.md.new.
|
|
389
|
+
#
|
|
390
|
+
# The manifest hash is always set to the template we shipped this run, including
|
|
391
|
+
# in the preserved case. That is what makes adoption self-healing: if the user
|
|
392
|
+
# later replaces their file with the .new one, the next run sees disk == manifest
|
|
393
|
+
# and resumes silent updates.
|
|
394
|
+
SKILLS_MANIFEST=".claude/.cc-discipline-skills.manifest"
|
|
395
|
+
NEW_MANIFEST=$(mktemp 2>/dev/null || echo ".claude/.skills-manifest.tmp")
|
|
396
|
+
PRESERVED_SKILLS=""
|
|
397
|
+
|
|
398
|
+
_cc_hash() {
|
|
399
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
|
|
400
|
+
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
|
|
401
|
+
elif command -v md5sum >/dev/null 2>&1; then md5sum "$1" | cut -d' ' -f1
|
|
402
|
+
elif command -v md5 >/dev/null 2>&1; then md5 -q "$1"
|
|
403
|
+
else cksum "$1" | cut -d' ' -f1; fi
|
|
404
|
+
}
|
|
405
|
+
|
|
380
406
|
for skill_dir in "$SCRIPT_DIR"/templates/.claude/skills/*/; do
|
|
381
407
|
[ -d "$skill_dir" ] || continue
|
|
382
408
|
skill_name=$(basename "$skill_dir")
|
|
383
409
|
mkdir -p ".claude/skills/$skill_name"
|
|
384
|
-
|
|
385
|
-
|
|
410
|
+
skill_status="updated"
|
|
411
|
+
|
|
412
|
+
for src in "$skill_dir"*; do
|
|
413
|
+
[ -f "$src" ] || continue
|
|
414
|
+
base=$(basename "$src")
|
|
415
|
+
rel="$skill_name/$base"
|
|
416
|
+
dst=".claude/skills/$rel"
|
|
417
|
+
tpl_hash=$(_cc_hash "$src")
|
|
418
|
+
|
|
419
|
+
if [ ! -f "$dst" ]; then
|
|
420
|
+
cp "$src" "$dst" # new file — nothing to protect
|
|
421
|
+
else
|
|
422
|
+
disk_hash=$(_cc_hash "$dst")
|
|
423
|
+
recorded=$(grep "^$rel " "$SKILLS_MANIFEST" 2>/dev/null | head -1 | cut -d' ' -f2)
|
|
424
|
+
if [ "$disk_hash" = "$tpl_hash" ]; then
|
|
425
|
+
: # already current
|
|
426
|
+
elif [ -n "$recorded" ] && [ "$disk_hash" = "$recorded" ]; then
|
|
427
|
+
cp "$src" "$dst" # pristine older version — safe
|
|
428
|
+
elif [ -z "$recorded" ]; then
|
|
429
|
+
# No manifest yet (first run on an existing install) and the file
|
|
430
|
+
# differs from the template. Cannot tell "older version" from
|
|
431
|
+
# "user edited", so assume the user's work matters.
|
|
432
|
+
cp "$src" "$dst.new"
|
|
433
|
+
skill_status="preserved"
|
|
434
|
+
else
|
|
435
|
+
cp "$src" "$dst.new" # user-modified — keep theirs
|
|
436
|
+
skill_status="preserved"
|
|
437
|
+
fi
|
|
438
|
+
fi
|
|
439
|
+
echo "$rel $tpl_hash" >> "$NEW_MANIFEST"
|
|
440
|
+
done
|
|
441
|
+
|
|
442
|
+
if [ "$skill_status" = "preserved" ]; then
|
|
443
|
+
PRESERVED_SKILLS="$PRESERVED_SKILLS $skill_name"
|
|
444
|
+
echo -e " ${YELLOW}~ /$skill_name (your version kept; new template at SKILL.md.new)${NC}"
|
|
445
|
+
else
|
|
446
|
+
echo " ✓ /$skill_name"
|
|
447
|
+
fi
|
|
386
448
|
done
|
|
387
449
|
|
|
450
|
+
[ -f "$NEW_MANIFEST" ] && mv "$NEW_MANIFEST" "$SKILLS_MANIFEST"
|
|
451
|
+
|
|
452
|
+
if [ -n "$PRESERVED_SKILLS" ]; then
|
|
453
|
+
echo -e " ${YELLOW}Locally modified skills were not overwritten:${PRESERVED_SKILLS}${NC}"
|
|
454
|
+
echo -e " ${YELLOW}Compare with: diff .claude/skills/<name>/SKILL.md{,.new}${NC}"
|
|
455
|
+
echo -e " ${YELLOW}Take the new one with: mv .claude/skills/<name>/SKILL.md{.new,}${NC}"
|
|
456
|
+
fi
|
|
457
|
+
|
|
388
458
|
# Clean up the stray file left behind by the v2.11.0–v2.12.0 bug above. A bare
|
|
389
459
|
# SKILL.md directly under skills/ is never a valid skill (skills live in
|
|
390
460
|
# subdirectories), so this only ever removes that debris.
|
package/package.json
CHANGED
|
@@ -43,42 +43,62 @@ if echo "$CMD_NORM" | grep -qE 'hooks[/\\][a-z-]+\.sh'; then
|
|
|
43
43
|
exit 0
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
+
# Blank out the CONTENT of message-style arguments before matching. A commit
|
|
47
|
+
# message that merely *describes* a destructive command is data — it can never
|
|
48
|
+
# execute — but the text match could not tell the difference, so writing about
|
|
49
|
+
# these commands blocked the commit:
|
|
50
|
+
# git commit -m "revert the git reset --hard change" -> must pass
|
|
51
|
+
# git commit -m "x" && git reset --hard -> must STILL block
|
|
52
|
+
#
|
|
53
|
+
# Only the argument to -m/--message/-F/--file is blanked, never quotes in
|
|
54
|
+
# general. Stripping all quoted text would open a real hole, because these DO
|
|
55
|
+
# execute what they quote and must keep matching:
|
|
56
|
+
# bash -c "git reset --hard" eval "git clean -fd" sudo git reset --hard
|
|
57
|
+
#
|
|
58
|
+
# Escaped quotes inside a message end the match early, leaving the tail to be
|
|
59
|
+
# scanned — that direction is a false positive (one wasted turn), never a miss.
|
|
60
|
+
SQ="'"
|
|
61
|
+
CMD_SCAN=$(printf '%s' "$CMD_NORM" \
|
|
62
|
+
| sed -E 's/(^|[[:space:]])(-m|--message|-F|--file)[[:space:]]*"[^"]*"/\1\2 ARG/g' \
|
|
63
|
+
| sed -E "s/(^|[[:space:]])(-m|--message|-F|--file)[[:space:]]*${SQ}[^${SQ}]*${SQ}/\1\2 ARG/g" \
|
|
64
|
+
| sed -E 's/(^|[[:space:]])(-m|--message|-F|--file)[[:space:]]+[^[:space:]]+/\1\2 ARG/g')
|
|
65
|
+
|
|
46
66
|
BLOCKED=""
|
|
47
67
|
SUGGESTION=""
|
|
48
68
|
|
|
49
69
|
# git checkout . / git checkout -- <file> (discard working tree changes)
|
|
50
70
|
# But allow: git checkout <branch>, git checkout -b <branch>
|
|
51
|
-
if echo "$
|
|
71
|
+
if echo "$CMD_SCAN" | grep -qE 'git\s+checkout\s+(\.|--\s)'; then
|
|
52
72
|
BLOCKED="git checkout (discards uncommitted changes)"
|
|
53
73
|
SUGGESTION="git stash"
|
|
54
74
|
fi
|
|
55
75
|
|
|
56
76
|
# git restore . / git restore <file> (without --staged)
|
|
57
|
-
if echo "$
|
|
77
|
+
if echo "$CMD_SCAN" | grep -qE 'git\s+restore\s' && ! echo "$CMD_SCAN" | grep -qE 'git\s+restore\s+--staged'; then
|
|
58
78
|
BLOCKED="git restore (discards uncommitted changes)"
|
|
59
79
|
SUGGESTION="git stash"
|
|
60
80
|
fi
|
|
61
81
|
|
|
62
82
|
# git reset --hard
|
|
63
|
-
if echo "$
|
|
83
|
+
if echo "$CMD_SCAN" | grep -qE 'git\s+reset\s+--hard'; then
|
|
64
84
|
BLOCKED="git reset --hard (destroys all uncommitted changes)"
|
|
65
85
|
SUGGESTION="git stash && git reset"
|
|
66
86
|
fi
|
|
67
87
|
|
|
68
88
|
# git clean -f / -fd / -fx
|
|
69
|
-
if echo "$
|
|
89
|
+
if echo "$CMD_SCAN" | grep -qE 'git\s+clean\s+-[a-z]*f'; then
|
|
70
90
|
BLOCKED="git clean -f (permanently deletes untracked files)"
|
|
71
91
|
SUGGESTION="git stash --include-untracked"
|
|
72
92
|
fi
|
|
73
93
|
|
|
74
94
|
# git branch -D (force delete unmerged branch)
|
|
75
|
-
if echo "$
|
|
95
|
+
if echo "$CMD_SCAN" | grep -qE 'git\s+branch\s+-D\s'; then
|
|
76
96
|
BLOCKED="git branch -D (deletes branch even if not merged)"
|
|
77
97
|
SUGGESTION="git branch -d (safe delete, fails if not merged)"
|
|
78
98
|
fi
|
|
79
99
|
|
|
80
100
|
# git push --force / -f (to main/master)
|
|
81
|
-
if echo "$
|
|
101
|
+
if echo "$CMD_SCAN" | grep -qE 'git\s+push\s+.*(-f|--force)' && echo "$CMD_SCAN" | grep -qE '(main|master)'; then
|
|
82
102
|
BLOCKED="git push --force to main/master (rewrites shared history)"
|
|
83
103
|
SUGGESTION="git push --force-with-lease"
|
|
84
104
|
fi
|