claude-code-achievements 1.2.2 → 1.2.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/bin/install.js CHANGED
@@ -15,7 +15,7 @@ const ITEMS_TO_COPY = ['.claude-plugin', 'commands', 'hooks', 'scripts', 'data',
15
15
  const GREEN = '\x1b[32m';
16
16
  const CYAN = '\x1b[36m';
17
17
  const YELLOW = '\x1b[33m';
18
- const RED = '\x1b[31m';
18
+ const MAGENTA = '\x1b[35m';
19
19
  const DIM = '\x1b[2m';
20
20
  const BOLD = '\x1b[1m';
21
21
  const RESET = '\x1b[0m';
@@ -54,14 +54,11 @@ function detectLanguage() {
54
54
  function checkSystemNotification(osName) {
55
55
  try {
56
56
  if (osName === 'macOS') {
57
- // macOS always has osascript
58
57
  return { available: true, method: 'osascript' };
59
58
  } else if (osName === 'Linux') {
60
- // Check for notify-send
61
59
  execSync('which notify-send', { stdio: 'ignore' });
62
60
  return { available: true, method: 'notify-send' };
63
61
  } else if (osName === 'Windows') {
64
- // Check for PowerShell
65
62
  execSync('where powershell.exe', { stdio: 'ignore' });
66
63
  return { available: true, method: 'PowerShell' };
67
64
  }
@@ -71,44 +68,54 @@ function checkSystemNotification(osName) {
71
68
  return { available: false, method: null };
72
69
  }
73
70
 
74
-
75
- async function prompt(question, options) {
71
+ async function selectOption(title, options, defaultIndex = 0) {
76
72
  const rl = readline.createInterface({
77
73
  input: process.stdin,
78
74
  output: process.stdout
79
75
  });
80
76
 
77
+ console.log(`\n${BOLD}${title}${RESET}\n`);
78
+
79
+ options.forEach((opt, i) => {
80
+ const marker = i === defaultIndex ? `${CYAN}▶${RESET}` : ' ';
81
+ const highlight = i === defaultIndex ? CYAN : DIM;
82
+ console.log(` ${marker} ${highlight}${i + 1}. ${opt.label}${RESET}`);
83
+ });
84
+
81
85
  return new Promise((resolve) => {
82
- const optStr = options.map((o, i) => `${i + 1}) ${o.label}`).join(' ');
83
- rl.question(`${question} [${optStr}]: `, (answer) => {
86
+ rl.question(`\n${DIM}Enter number [1-${options.length}]:${RESET} `, (answer) => {
84
87
  rl.close();
85
- const num = parseInt(answer) || 1;
88
+ const num = parseInt(answer) || (defaultIndex + 1);
86
89
  const idx = Math.max(0, Math.min(options.length - 1, num - 1));
87
- resolve(options[idx].value);
90
+ resolve({ value: options[idx].value, label: options[idx].label });
88
91
  });
89
92
  });
90
93
  }
91
94
 
92
95
  async function install() {
93
96
  console.log('');
94
- console.log(`${CYAN}${BOLD}╔═══════════════════════════════════════════════════════════╗${RESET}`);
95
- console.log(`${CYAN}${BOLD} 🎮 Claude Code Achievements Installer 🎮 ║${RESET}`);
96
- console.log(`${CYAN}${BOLD}╚═══════════════════════════════════════════════════════════╝${RESET}`);
97
+ console.log(`${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`);
98
+ console.log(`${CYAN}║ ║${RESET}`);
99
+ console.log(`${CYAN}║${RESET} ${BOLD}🎮 Claude Code Achievements${RESET} ${CYAN}║${RESET}`);
100
+ console.log(`${CYAN}║${RESET} ${DIM}Level up your AI coding skills${RESET} ${CYAN}║${RESET}`);
101
+ console.log(`${CYAN}║ ║${RESET}`);
102
+ console.log(`${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}`);
97
103
  console.log('');
98
104
 
99
105
  const detectedOS = detectOS();
100
106
  const detectedLang = detectLanguage();
101
107
  const notifyCheck = checkSystemNotification(detectedOS);
102
108
 
103
- console.log(`${DIM}Detected OS: ${detectedOS}${RESET}`);
109
+ // System info box
110
+ console.log(`${DIM}┌─ System Info ─────────────────────────────────────────────────┐${RESET}`);
111
+ console.log(`${DIM}│${RESET} Platform: ${BOLD}${detectedOS}${RESET}`);
104
112
  if (notifyCheck.available) {
105
- console.log(`${DIM}System notifications: ${GREEN}✓${RESET}${DIM} ${notifyCheck.method}${RESET}`);
113
+ console.log(`${DIM}│${RESET} Notifications: ${GREEN}✓ Available${RESET} ${DIM}(${notifyCheck.method})${RESET}`);
106
114
  } else {
107
- console.log(`${DIM}System notifications: ${YELLOW}✗${RESET}${DIM} not available (will use terminal)${RESET}`);
115
+ console.log(`${DIM}│${RESET} Notifications: ${YELLOW} Terminal only${RESET}`);
108
116
  }
109
- console.log('');
117
+ console.log(`${DIM}└───────────────────────────────────────────────────────────────┘${RESET}`);
110
118
 
111
- // Check if interactive
112
119
  const isInteractive = process.stdin.isTTY;
113
120
 
114
121
  let language = detectedLang;
@@ -116,43 +123,49 @@ async function install() {
116
123
 
117
124
  if (isInteractive) {
118
125
  // Language selection
119
- console.log(`${BOLD}Select language / 选择语言 / Idioma / 언어 / 言語:${RESET}`);
120
- language = await prompt('', [
121
- { label: 'English', value: 'en' },
122
- { label: '中文', value: 'zh' },
123
- { label: 'Español', value: 'es' },
124
- { label: '한국어', value: 'ko' },
125
- { label: '日本語', value: 'ja' }
126
- ]);
127
- const langNames = { en: 'English', zh: '中文', es: 'Español', ko: '한국어', ja: '日本語' };
128
- console.log(` ${GREEN}✓${RESET} ${langNames[language]}`);
129
- console.log('');
130
-
131
- // Notification style (only ask if system notifications available)
126
+ const langResult = await selectOption(
127
+ '🌍 Choose your language',
128
+ [
129
+ { label: '🇺🇸 English', value: 'en' },
130
+ { label: '🇨🇳 中文', value: 'zh' },
131
+ { label: '🇪🇸 Español', value: 'es' },
132
+ { label: '🇰🇷 한국어', value: 'ko' },
133
+ { label: '🇯🇵 日本語', value: 'ja' }
134
+ ],
135
+ ['en', 'zh', 'es', 'ko', 'ja'].indexOf(detectedLang)
136
+ );
137
+ language = langResult.value;
138
+ console.log(`${GREEN} ✓ Selected: ${langResult.label}${RESET}`);
139
+
140
+ // Notification style
132
141
  if (notifyCheck.available) {
133
- console.log(`${BOLD}Notification style:${RESET}`);
134
- notificationStyle = await prompt('', [
135
- { label: `System (${notifyCheck.method})`, value: 'system' },
136
- { label: 'Terminal', value: 'terminal' },
137
- { label: 'Both', value: 'both' }
138
- ]);
139
- console.log(` ${GREEN}✓${RESET} ${notificationStyle}`);
142
+ const notifyResult = await selectOption(
143
+ '🔔 Achievement notifications',
144
+ [
145
+ { label: `System popup (${notifyCheck.method})`, value: 'system' },
146
+ { label: 'Terminal message', value: 'terminal' },
147
+ { label: 'Both', value: 'both' }
148
+ ],
149
+ 0
150
+ );
151
+ notificationStyle = notifyResult.value;
152
+ console.log(`${GREEN} ✓ Selected: ${notifyResult.label}${RESET}`);
140
153
  } else {
141
- console.log(`${BOLD}Notification:${RESET} Terminal (system not available)`);
142
- notificationStyle = 'terminal';
154
+ console.log(`\n${DIM}🔔 Notifications: Terminal mode (system not available)${RESET}`);
143
155
  }
144
- console.log('');
145
156
  }
146
157
 
158
+ console.log('');
159
+ console.log(`${DIM}───────────────────────────────────────────────────────────────${RESET}`);
160
+ console.log(`${YELLOW} ⏳ Installing plugin...${RESET}`);
161
+
147
162
  // Copy files
148
163
  const packageRoot = path.resolve(__dirname, '..');
149
164
 
150
165
  if (fs.existsSync(TARGET_DIR)) {
151
- console.log(`${DIM}Removing existing installation...${RESET}`);
152
166
  fs.rmSync(TARGET_DIR, { recursive: true, force: true });
153
167
  }
154
168
 
155
- console.log(`${DIM}Installing to ${TARGET_DIR}${RESET}`);
156
169
  fs.mkdirSync(TARGET_DIR, { recursive: true });
157
170
 
158
171
  for (const item of ITEMS_TO_COPY) {
@@ -171,7 +184,6 @@ async function install() {
171
184
  fs.mkdirSync(stateDir, { recursive: true });
172
185
  }
173
186
 
174
- // Always update settings, preserve achievements if exists
175
187
  let existingState = null;
176
188
  if (fs.existsSync(stateFile)) {
177
189
  try {
@@ -192,8 +204,7 @@ async function install() {
192
204
 
193
205
  fs.writeFileSync(stateFile, JSON.stringify(newState, null, 2));
194
206
 
195
- // Symlink commands to ~/.claude/commands/ for short command names (/achievements)
196
- // Plugin also provides namespaced commands (/claude-code-achievements:achievements)
207
+ // Symlink commands
197
208
  const commandsDir = path.join(os.homedir(), '.claude', 'commands');
198
209
  if (!fs.existsSync(commandsDir)) {
199
210
  fs.mkdirSync(commandsDir, { recursive: true });
@@ -204,17 +215,14 @@ async function install() {
204
215
  const linkPath = path.join(commandsDir, file);
205
216
  const targetPath = path.join(pluginCommands, file);
206
217
 
207
- // Remove existing file/symlink if exists
208
218
  if (fs.existsSync(linkPath)) {
209
219
  fs.unlinkSync(linkPath);
210
220
  }
211
-
212
- // Create symlink
213
221
  fs.symlinkSync(targetPath, linkPath);
214
222
  }
215
223
  }
216
224
 
217
- // Register hooks in ~/.claude/settings.json
225
+ // Register hooks
218
226
  const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
219
227
  let settings = {};
220
228
  if (fs.existsSync(settingsFile)) {
@@ -223,11 +231,9 @@ async function install() {
223
231
  } catch (e) {}
224
232
  }
225
233
 
226
- // Add plugin to enabledPlugins
227
234
  if (!settings.enabledPlugins) settings.enabledPlugins = {};
228
235
  settings.enabledPlugins['claude-code-achievements@local'] = true;
229
236
 
230
- // Add hooks (replace any existing achievement hooks)
231
237
  if (!settings.hooks) settings.hooks = {};
232
238
 
233
239
  const trackScript = path.join(TARGET_DIR, 'hooks', 'track-achievement.sh');
@@ -249,19 +255,31 @@ async function install() {
249
255
 
250
256
  fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
251
257
 
258
+ // Count achievements if any exist
259
+ const achievementCount = Object.keys(existingState?.achievements || {}).length;
260
+
261
+ console.log(`${DIM}───────────────────────────────────────────────────────────────${RESET}`);
252
262
  console.log('');
253
- console.log(`${GREEN}${BOLD}✅ Installation complete!${RESET}`);
254
- console.log('');
255
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
256
- console.log('');
257
- console.log(`${BOLD}Ready to use:${RESET}`);
263
+ console.log(`${GREEN}${BOLD} ✅ Installation complete!${RESET}`);
258
264
  console.log('');
259
- console.log(` ${CYAN}/achievements${RESET} View your achievements`);
260
- console.log(` ${CYAN}/achievements hint${RESET} Get tips for unlocking`);
265
+
266
+ if (achievementCount > 0) {
267
+ console.log(`${MAGENTA} 🏆 Welcome back! You have ${achievementCount} achievement${achievementCount > 1 ? 's' : ''} unlocked.${RESET}`);
268
+ console.log('');
269
+ }
270
+
271
+ console.log(`${CYAN}╭──────────────────────────────────────────────────────────────╮${RESET}`);
272
+ console.log(`${CYAN}│${RESET} ${BOLD}Quick Start${RESET} ${CYAN}│${RESET}`);
273
+ console.log(`${CYAN}│${RESET} ${CYAN}│${RESET}`);
274
+ console.log(`${CYAN}│${RESET} ${YELLOW}/achievements${RESET} View your achievements ${CYAN}│${RESET}`);
275
+ console.log(`${CYAN}│${RESET} ${YELLOW}/achievements locked${RESET} See what's left to unlock ${CYAN}│${RESET}`);
276
+ console.log(`${CYAN}│${RESET} ${YELLOW}/achievements-settings${RESET} Change language & notifications ${CYAN}│${RESET}`);
277
+ console.log(`${CYAN}│${RESET} ${CYAN}│${RESET}`);
278
+ console.log(`${CYAN}╰──────────────────────────────────────────────────────────────╯${RESET}`);
261
279
  console.log('');
262
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
280
+ console.log(`${DIM} 26 achievements await. Start coding to unlock them!${RESET}`);
263
281
  console.log('');
264
- console.log('🎮 Happy coding!');
282
+ console.log(` ${BOLD}🎮 Happy coding!${RESET}`);
265
283
  console.log('');
266
284
  }
267
285
 
@@ -169,7 +169,8 @@ check_achievements() {
169
169
  # visual_inspector (image files) - case insensitive
170
170
  FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
171
171
  FILE_NAME=$(basename "${FILE_PATH}")
172
- FILE_PATH_LOWER="${FILE_PATH,,}"
172
+ # Use tr for POSIX compatibility (bash 4.0+ ${,,} not available everywhere)
173
+ FILE_PATH_LOWER=$(echo "${FILE_PATH}" | tr '[:upper:]' '[:lower:]')
173
174
  if [[ "${FILE_PATH_LOWER}" =~ \.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$ ]]; then
174
175
  if ! is_unlocked "visual_inspector"; then
175
176
  unlock_achievement "visual_inspector" "Analyzed image: ${FILE_NAME}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-achievements",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Steam-style achievement system for Claude Code - gamify your coding journey!",
5
5
  "author": "subinium",
6
6
  "license": "MIT",