claude-code-achievements 1.0.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.
@@ -0,0 +1,94 @@
1
+ {
2
+ "ui": {
3
+ "title": "CLAUDE CODE ACHIEVEMENTS",
4
+ "unlocked": "업적 달성!",
5
+ "locked": "잠김",
6
+ "progress": "진행 상황",
7
+ "rarity": "희귀도",
8
+ "tip": "팁",
9
+ "hint": "힌트"
10
+ },
11
+ "categories": {
12
+ "basics": { "name": "시작하기", "description": "Claude Code 첫걸음" },
13
+ "workflow": { "name": "워크플로우", "description": "개발 워크플로우 마스터" },
14
+ "tools": { "name": "파워 툴", "description": "고급 기능 활용" },
15
+ "mastery": { "name": "마스터리", "description": "전문가 수준" }
16
+ },
17
+ "achievements": {
18
+ "first_edit": {
19
+ "name": "첫 터치",
20
+ "description": "Claude와 함께 파일 편집하기",
21
+ "tip": "구체적으로 말하세요: '버그 고쳐줘' 대신 'login.js 42번 줄의 TypeError 수정해줘'"
22
+ },
23
+ "first_write": {
24
+ "name": "창조자",
25
+ "description": "새 파일 생성하기",
26
+ "tip": "Claude는 설명만으로 전체 파일을 만들 수 있어요. '로그인 폼 React 컴포넌트 만들어줘'"
27
+ },
28
+ "claude_md_creator": {
29
+ "name": "프로젝트 큐레이터",
30
+ "description": "CLAUDE.md로 프로젝트 컨텍스트 설정",
31
+ "tip": "CLAUDE.md는 프로젝트 이해를 돕습니다. 기술 스택, 코딩 스타일, 주요 파일, 자주 쓰는 명령어를 포함하세요."
32
+ },
33
+ "plan_mode_user": {
34
+ "name": "전략적 사고가",
35
+ "description": "복잡한 작업에 Plan 모드 사용",
36
+ "tip": "Shift+Tab 두 번으로 Plan 모드 진입. 아키텍처 결정이나 다단계 작업에 좋아요."
37
+ },
38
+ "git_commit": {
39
+ "name": "버전 관리자",
40
+ "description": "Claude와 함께 커밋하기",
41
+ "tip": "Claude가 커밋 메시지를 작성하게 하세요! conventional commits 형식으로 작성합니다."
42
+ },
43
+ "git_push": {
44
+ "name": "배포하기!",
45
+ "description": "원격 저장소에 푸시",
46
+ "tip": "푸시 전에 항상 변경 사항을 검토하세요. Claude에게 요약을 요청하세요."
47
+ },
48
+ "run_tests": {
49
+ "name": "품질 수호자",
50
+ "description": "Claude와 함께 테스트 실행",
51
+ "tip": "'테스트 실행하고 실패하면 고쳐줘'. 이게 바이브코딩 검증 루프입니다!"
52
+ },
53
+ "multi_agent": {
54
+ "name": "위임의 달인",
55
+ "description": "Task 도구로 서브 에이전트 생성",
56
+ "tip": "Task 도구는 복잡한 작업을 위한 전문 에이전트를 만듭니다."
57
+ },
58
+ "first_mcp": {
59
+ "name": "MCP 개척자",
60
+ "description": "MCP 도구 사용하기",
61
+ "tip": "MCP는 Claude 능력을 확장합니다. DB, API, 커스텀 도구에 연결하세요."
62
+ },
63
+ "web_searcher": {
64
+ "name": "웹 탐험가",
65
+ "description": "웹 검색하기",
66
+ "tip": "Claude가 최신 문서, 솔루션, API를 검색할 수 있어요."
67
+ },
68
+ "skill_invoker": {
69
+ "name": "스킬 마스터",
70
+ "description": "슬래시 명령 스킬 사용",
71
+ "tip": "/commit, /pr, /init 해보세요. ~/.claude/commands/에서 커스텀도 가능!"
72
+ },
73
+ "notebook_editor": {
74
+ "name": "데이터 과학자",
75
+ "description": "Jupyter 노트북 편집",
76
+ "tip": "Claude는 노트북을 이해해요! 데이터 분석, 시각화, ML 실험을 요청하세요."
77
+ },
78
+ "config_modifier": {
79
+ "name": "커스터마이저",
80
+ "description": "Claude Code 설정 수정",
81
+ "tip": "/config 또는 ~/.claude/settings.json을 편집하세요."
82
+ },
83
+ "hooks_user": {
84
+ "name": "자동화 아키텍트",
85
+ "description": "Claude Code 훅 설정",
86
+ "tip": "훅은 이벤트에 스크립트를 실행해요. 린팅, 테스트, 커스텀 워크플로우에 활용!"
87
+ },
88
+ "ralph_starter": {
89
+ "name": "루프 마스터",
90
+ "description": "자율 코딩 루프 시작",
91
+ "tip": "Ralph Loop는 목표 달성까지 Claude가 자율적으로 작업합니다."
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": ".*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/track-achievement.sh",
10
+ "timeout": 5
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "Stop": [
16
+ {
17
+ "matcher": ".*",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/track-stop.sh",
22
+ "timeout": 5
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,178 @@
1
+ #!/bin/bash
2
+ # track-achievement.sh - Main achievement tracking logic for PostToolUse hook
3
+ # Receives JSON via stdin with tool_name, tool_input, and permission_mode
4
+
5
+ set -e
6
+
7
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
8
+ STATE_DIR="${HOME}/.claude/achievements"
9
+ STATE_FILE="${STATE_DIR}/state.json"
10
+ ACHIEVEMENTS_FILE="${PLUGIN_ROOT}/data/achievements.json"
11
+
12
+ # Initialize state file if it doesn't exist
13
+ init_state() {
14
+ mkdir -p "${STATE_DIR}"
15
+ if [[ ! -f "${STATE_FILE}" ]]; then
16
+ cat > "${STATE_FILE}" << 'EOF'
17
+ {
18
+ "settings": { "language": "en", "notifications": true, "notification_style": "system" },
19
+ "achievements": {},
20
+ "counters": { "ralph_iterations": 0 },
21
+ "session": { "files_read_set": [] }
22
+ }
23
+ EOF
24
+ fi
25
+ }
26
+
27
+ # Read input from stdin
28
+ read_input() {
29
+ INPUT=$(cat)
30
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
31
+ TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // {}')
32
+ PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
33
+ }
34
+
35
+ # Check if achievement is already unlocked
36
+ is_unlocked() {
37
+ local achievement_id="$1"
38
+ jq -e ".achievements[\"${achievement_id}\"].unlocked == true" "${STATE_FILE}" > /dev/null 2>&1
39
+ }
40
+
41
+ # Unlock an achievement
42
+ unlock_achievement() {
43
+ local achievement_id="$1"
44
+ local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
45
+ local temp_file=$(mktemp)
46
+ jq ".achievements[\"${achievement_id}\"] = {\"unlocked\": true, \"unlockedAt\": \"${timestamp}\"}" "${STATE_FILE}" > "${temp_file}"
47
+ mv "${temp_file}" "${STATE_FILE}"
48
+ "${PLUGIN_ROOT}/scripts/show-notification.sh" "${achievement_id}"
49
+ }
50
+
51
+ # Check and unlock achievements based on tool use
52
+ check_achievements() {
53
+ # Plan mode achievement
54
+ if [[ "${PERMISSION_MODE}" == "plan" ]]; then
55
+ if ! is_unlocked "plan_mode_user"; then
56
+ unlock_achievement "plan_mode_user"
57
+ fi
58
+ fi
59
+
60
+ case "${TOOL_NAME}" in
61
+ Write)
62
+ # first_write
63
+ if ! is_unlocked "first_write"; then
64
+ unlock_achievement "first_write"
65
+ fi
66
+
67
+ FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
68
+
69
+ # claude_md_creator
70
+ if [[ "${FILE_PATH}" =~ CLAUDE\.md$ ]]; then
71
+ if ! is_unlocked "claude_md_creator"; then
72
+ unlock_achievement "claude_md_creator"
73
+ fi
74
+ fi
75
+
76
+ # config_modifier
77
+ if [[ "${FILE_PATH}" =~ \.claude/settings.*\.json$ ]]; then
78
+ if ! is_unlocked "config_modifier"; then
79
+ unlock_achievement "config_modifier"
80
+ fi
81
+ fi
82
+
83
+ # hooks_user (hooks in settings or hooks.json)
84
+ if [[ "${FILE_PATH}" =~ hooks\.json$ ]] || [[ "${FILE_PATH}" =~ \.claude/settings.*\.json$ ]]; then
85
+ # Check if file contains hooks configuration
86
+ if [[ -f "${FILE_PATH}" ]]; then
87
+ if grep -q '"hooks"' "${FILE_PATH}" 2>/dev/null || grep -q '"PostToolUse"' "${FILE_PATH}" 2>/dev/null; then
88
+ if ! is_unlocked "hooks_user"; then
89
+ unlock_achievement "hooks_user"
90
+ fi
91
+ fi
92
+ fi
93
+ fi
94
+ ;;
95
+
96
+ Edit)
97
+ # first_edit
98
+ if ! is_unlocked "first_edit"; then
99
+ unlock_achievement "first_edit"
100
+ fi
101
+ ;;
102
+
103
+ Bash)
104
+ COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
105
+
106
+ # git_commit (git add/commit)
107
+ if [[ "${COMMAND}" =~ git[[:space:]]+(add|commit) ]]; then
108
+ if ! is_unlocked "git_commit"; then
109
+ unlock_achievement "git_commit"
110
+ fi
111
+ fi
112
+
113
+ # git_push
114
+ if [[ "${COMMAND}" =~ git[[:space:]]+push ]]; then
115
+ if ! is_unlocked "git_push"; then
116
+ unlock_achievement "git_push"
117
+ fi
118
+ fi
119
+
120
+ # run_tests
121
+ if [[ "${COMMAND}" =~ (npm|yarn|pnpm)[[:space:]]+(test|vitest|jest) ]] || \
122
+ [[ "${COMMAND}" =~ pytest ]] || \
123
+ [[ "${COMMAND}" =~ go[[:space:]]+test ]] || \
124
+ [[ "${COMMAND}" =~ cargo[[:space:]]+test ]] || \
125
+ [[ "${COMMAND}" =~ bun[[:space:]]+test ]]; then
126
+ if ! is_unlocked "run_tests"; then
127
+ unlock_achievement "run_tests"
128
+ fi
129
+ fi
130
+ ;;
131
+
132
+ Task)
133
+ if ! is_unlocked "multi_agent"; then
134
+ unlock_achievement "multi_agent"
135
+ fi
136
+ ;;
137
+
138
+ Skill)
139
+ if ! is_unlocked "skill_invoker"; then
140
+ unlock_achievement "skill_invoker"
141
+ fi
142
+
143
+ # ralph_starter
144
+ SKILL_NAME=$(echo "$TOOL_INPUT" | jq -r '.skill // empty')
145
+ if [[ "${SKILL_NAME}" =~ ralph-loop|ralph ]]; then
146
+ if ! is_unlocked "ralph_starter"; then
147
+ unlock_achievement "ralph_starter"
148
+ fi
149
+ fi
150
+ ;;
151
+
152
+ WebSearch)
153
+ if ! is_unlocked "web_searcher"; then
154
+ unlock_achievement "web_searcher"
155
+ fi
156
+ ;;
157
+
158
+ NotebookEdit)
159
+ if ! is_unlocked "notebook_editor"; then
160
+ unlock_achievement "notebook_editor"
161
+ fi
162
+ ;;
163
+
164
+ mcp__*)
165
+ # Any MCP tool
166
+ if ! is_unlocked "first_mcp"; then
167
+ unlock_achievement "first_mcp"
168
+ fi
169
+ ;;
170
+ esac
171
+ }
172
+
173
+ # Main
174
+ init_state
175
+ read_input
176
+ check_achievements
177
+
178
+ exit 0
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # track-stop.sh - Track Stop hook for ralph-loop iterations
3
+ # Called when a conversation ends or is interrupted
4
+
5
+ set -e
6
+
7
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
8
+ STATE_DIR="${HOME}/.claude/achievements"
9
+ STATE_FILE="${STATE_DIR}/state.json"
10
+
11
+ # Check if state file exists
12
+ if [[ ! -f "${STATE_FILE}" ]]; then
13
+ exit 0
14
+ fi
15
+
16
+ # Read input from stdin
17
+ INPUT=$(cat)
18
+ STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
19
+
20
+ # Increment ralph iterations if ralph-loop is active
21
+ # This is detected by checking if the session has ralph_loop_active flag
22
+ RALPH_ACTIVE=$(jq -r '.session.ralph_loop_active // false' "${STATE_FILE}")
23
+
24
+ if [[ "${RALPH_ACTIVE}" == "true" ]]; then
25
+ # Increment counter
26
+ temp_file=$(mktemp)
27
+ jq '.counters.ralph_iterations = (.counters.ralph_iterations // 0) + 1' "${STATE_FILE}" > "${temp_file}"
28
+ mv "${temp_file}" "${STATE_FILE}"
29
+
30
+ # Check if threshold reached
31
+ RALPH_COUNT=$(jq -r '.counters.ralph_iterations' "${STATE_FILE}")
32
+ if [[ "${RALPH_COUNT}" -ge 100 ]]; then
33
+ # Check if already unlocked
34
+ if ! jq -e '.achievements["ralph_master"].unlocked == true' "${STATE_FILE}" > /dev/null 2>&1; then
35
+ "${PLUGIN_ROOT}/scripts/show-notification.sh" "ralph_master"
36
+ temp_file=$(mktemp)
37
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
38
+ jq ".achievements[\"ralph_master\"] = {\"unlocked\": true, \"unlockedAt\": \"${timestamp}\"}" "${STATE_FILE}" > "${temp_file}"
39
+ mv "${temp_file}" "${STATE_FILE}"
40
+ fi
41
+ fi
42
+ fi
43
+
44
+ exit 0
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "claude-code-achievements",
3
+ "version": "1.0.0",
4
+ "description": "Steam-style achievement system for Claude Code - gamify your coding journey!",
5
+ "author": "subinium",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/subinium/claude-code-achievements.git"
10
+ },
11
+ "homepage": "https://github.com/subinium/claude-code-achievements#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/subinium/claude-code-achievements/issues"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "achievements",
19
+ "gamification",
20
+ "vibecoding",
21
+ "plugin",
22
+ "cli"
23
+ ],
24
+ "bin": {
25
+ "claude-code-achievements": "bin/install.js"
26
+ },
27
+ "files": [
28
+ ".claude-plugin",
29
+ "commands/",
30
+ "hooks/",
31
+ "scripts/",
32
+ "data/",
33
+ "bin/"
34
+ ],
35
+ "engines": {
36
+ "node": ">=14.0.0"
37
+ }
38
+ }
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # init-state.sh - Initialize or reset the achievements state file
3
+ # Usage: init-state.sh [--reset]
4
+
5
+ set -e
6
+
7
+ STATE_DIR="${HOME}/.claude/achievements"
8
+ STATE_FILE="${STATE_DIR}/state.json"
9
+
10
+ # Check for reset flag
11
+ if [[ "$1" == "--reset" ]]; then
12
+ rm -f "${STATE_FILE}"
13
+ echo "State file reset."
14
+ fi
15
+
16
+ # Create directory if needed
17
+ mkdir -p "${STATE_DIR}"
18
+
19
+ # Create initial state if doesn't exist
20
+ if [[ ! -f "${STATE_FILE}" ]]; then
21
+ cat > "${STATE_FILE}" << 'EOF'
22
+ {
23
+ "settings": {
24
+ "language": "en",
25
+ "notifications": true
26
+ },
27
+ "achievements": {},
28
+ "counters": {
29
+ "ralph_iterations": 0,
30
+ "files_read": 0,
31
+ "edits_made": 0
32
+ },
33
+ "session": {
34
+ "files_read_set": [],
35
+ "ralph_loop_active": false
36
+ }
37
+ }
38
+ EOF
39
+ echo "State file initialized at: ${STATE_FILE}"
40
+ else
41
+ echo "State file already exists at: ${STATE_FILE}"
42
+ fi
43
+
44
+ exit 0
@@ -0,0 +1,152 @@
1
+ #!/bin/bash
2
+ # show-achievements.sh - Display achievements in a clean TUI-style view
3
+
4
+ set -e
5
+
6
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
7
+ STATE_FILE="${HOME}/.claude/achievements/state.json"
8
+ ACHIEVEMENTS_FILE="${PLUGIN_ROOT}/data/achievements.json"
9
+
10
+ # Initialize state if needed
11
+ if [[ ! -f "${STATE_FILE}" ]]; then
12
+ mkdir -p "$(dirname "${STATE_FILE}")"
13
+ cat > "${STATE_FILE}" << 'EOF'
14
+ {
15
+ "settings": { "language": "en", "notifications": true, "notification_style": "system" },
16
+ "achievements": {},
17
+ "counters": { "ralph_iterations": 0 },
18
+ "session": { "files_read_set": [] }
19
+ }
20
+ EOF
21
+ fi
22
+
23
+ # Get user's language preference
24
+ LANG=$(jq -r '.settings.language // "en"' "${STATE_FILE}")
25
+ I18N_FILE="${PLUGIN_ROOT}/data/i18n/${LANG}.json"
26
+
27
+ # Function to get localized text
28
+ get_localized() {
29
+ local key="$1"
30
+ local fallback="$2"
31
+ local result=""
32
+ if [[ -f "${I18N_FILE}" ]]; then
33
+ result=$(jq -r "${key} // empty" "${I18N_FILE}" 2>/dev/null)
34
+ fi
35
+ if [[ -z "${result}" || "${result}" == "null" ]]; then
36
+ echo "${fallback}"
37
+ else
38
+ echo "${result}"
39
+ fi
40
+ }
41
+
42
+ get_achievement_name() {
43
+ local id="$1"
44
+ get_localized ".achievements[\"${id}\"].name" "$(jq -r ".achievements[\"${id}\"].name" "${ACHIEVEMENTS_FILE}")"
45
+ }
46
+
47
+ get_achievement_desc() {
48
+ local id="$1"
49
+ get_localized ".achievements[\"${id}\"].description" "$(jq -r ".achievements[\"${id}\"].description" "${ACHIEVEMENTS_FILE}")"
50
+ }
51
+
52
+ # Get rarity emoji
53
+ get_rarity_emoji() {
54
+ case "$1" in
55
+ common) echo "⬜" ;;
56
+ uncommon) echo "🟩" ;;
57
+ rare) echo "🟦" ;;
58
+ epic) echo "🟪" ;;
59
+ legendary) echo "🟨" ;;
60
+ *) echo "⬜" ;;
61
+ esac
62
+ }
63
+
64
+ # Calculate stats
65
+ print_header() {
66
+ local total=$(jq '.achievements | length' "${ACHIEVEMENTS_FILE}")
67
+ local unlocked=$(jq '.achievements | to_entries | map(select(.value.unlocked == true)) | length' "${STATE_FILE}")
68
+ local percent=0
69
+ [[ ${total} -gt 0 ]] && percent=$((unlocked * 100 / total))
70
+
71
+ echo ""
72
+ echo "🎮 CLAUDE CODE ACHIEVEMENTS"
73
+ echo "┌────────────────────────────────────────────────────────┐"
74
+ echo "│"
75
+
76
+ # Progress bar
77
+ local filled=$((percent / 5))
78
+ local empty=$((20 - filled))
79
+ printf "│ "
80
+ for ((i=0; i<filled; i++)); do printf "█"; done
81
+ for ((i=0; i<empty; i++)); do printf "░"; done
82
+ printf " %d/%d unlocked (%d%%)\n" "${unlocked}" "${total}" "${percent}"
83
+ echo "│"
84
+ }
85
+
86
+ # Show achievements for a category
87
+ show_category() {
88
+ local category="$1"
89
+ local category_name=$(jq -r ".categories[\"${category}\"].name // \"${category}\"" "${ACHIEVEMENTS_FILE}")
90
+ local localized_name=$(get_localized ".categories[\"${category}\"].name" "${category_name}")
91
+
92
+ echo "├── ${localized_name} ─────────────────────────────────────"
93
+ echo "│"
94
+
95
+ # Get achievements in this category
96
+ local ids=$(jq -r ".achievements | to_entries | map(select(.value.category == \"${category}\")) | .[].key" "${ACHIEVEMENTS_FILE}")
97
+
98
+ for id in ${ids}; do
99
+ local icon=$(jq -r ".achievements[\"${id}\"].icon // \"🏆\"" "${ACHIEVEMENTS_FILE}")
100
+ local rarity=$(jq -r ".achievements[\"${id}\"].rarity // \"common\"" "${ACHIEVEMENTS_FILE}")
101
+ local rarity_emoji=$(get_rarity_emoji "${rarity}")
102
+ local name=$(get_achievement_name "${id}")
103
+ local desc=$(get_achievement_desc "${id}")
104
+
105
+ local unlocked="false"
106
+ if jq -e ".achievements[\"${id}\"].unlocked == true" "${STATE_FILE}" > /dev/null 2>&1; then
107
+ unlocked="true"
108
+ fi
109
+
110
+ if [[ "${unlocked}" == "true" ]]; then
111
+ echo "│ ✅ ${icon} ${name} ${rarity_emoji}"
112
+ else
113
+ echo "│ ⬛ ${icon} ${name} ${rarity_emoji}"
114
+ echo "│ ↳ ${desc}"
115
+ fi
116
+ done
117
+ echo "│"
118
+ }
119
+
120
+ # Show rarity legend
121
+ show_legend() {
122
+ echo "├────────────────────────────────────────────────────────┤"
123
+ echo "│ ⬜ Common 🟩 Uncommon 🟦 Rare 🟪 Epic 🟨 Legendary"
124
+ echo "└────────────────────────────────────────────────────────┘"
125
+ echo ""
126
+ }
127
+
128
+ # Main
129
+ ARG="${1:-all}"
130
+
131
+ print_header
132
+
133
+ case "${ARG}" in
134
+ stats)
135
+ show_legend
136
+ ;;
137
+ all)
138
+ for category in $(jq -r '.categories | to_entries | sort_by(.value.order) | .[].key' "${ACHIEVEMENTS_FILE}"); do
139
+ show_category "${category}"
140
+ done
141
+ show_legend
142
+ ;;
143
+ *)
144
+ if jq -e ".categories[\"${ARG}\"]" "${ACHIEVEMENTS_FILE}" > /dev/null 2>&1; then
145
+ show_category "${ARG}"
146
+ show_legend
147
+ else
148
+ echo "Unknown: ${ARG}"
149
+ echo "Available: basics, workflow, tools, mastery"
150
+ fi
151
+ ;;
152
+ esac