@yeongjaeyou/claude-code-config 0.12.0 → 0.14.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/.claude/guidelines/work-guidelines.md +33 -1
- package/.claude/hooks/notify_osc.sh +53 -0
- package/README.md +21 -0
- package/bin/cli.js +18 -190
- package/lib/cli-utils.js +271 -0
- package/package.json +15 -2
|
@@ -13,10 +13,42 @@ Common guidelines for all Claude Code commands and development workflow.
|
|
|
13
13
|
- Do not use emoji in code or documentation
|
|
14
14
|
- Never add Claude attribution (e.g., "Generated with Claude", "Co-Authored-By: Claude") to commits, PRs, or issues
|
|
15
15
|
|
|
16
|
+
### Writing Style (Anti-AI)
|
|
17
|
+
Write like a human, not a chatbot. Applies to ALL text: responses, documentation, comments, commit messages.
|
|
18
|
+
|
|
19
|
+
**NEVER use these AI-ish patterns:**
|
|
20
|
+
- Filler openers: "Certainly!", "Of course!", "Absolutely!", "I'd be happy to", "Great question!"
|
|
21
|
+
- Excessive affirmation: "That's a great idea", "You're absolutely right", "Excellent point"
|
|
22
|
+
- Redundant summaries: "To summarize...", "In conclusion...", "To recap..."
|
|
23
|
+
- Over-explanation: Explaining obvious things, restating the question
|
|
24
|
+
- Hedging phrases: "I think maybe...", "It might be possible that..."
|
|
25
|
+
- Hollow transitions: "Now, let's...", "Moving on to...", "Next, we'll..."
|
|
26
|
+
- Colon headers: "**항목:** 설명", "**Topic:** content" - just write naturally without label-colon-content structure
|
|
27
|
+
|
|
28
|
+
**DO:**
|
|
29
|
+
- Get to the point immediately
|
|
30
|
+
- Be direct and concise
|
|
31
|
+
- Use natural, conversational tone
|
|
32
|
+
- Skip pleasantries unless genuinely warranted
|
|
33
|
+
|
|
16
34
|
### Uncertainty Handling
|
|
17
35
|
- If you are uncertain, confused, or lack clarity about the requirements or approach, **STOP immediately** and ask the user for clarification
|
|
18
36
|
- Do not proceed with assumptions or guesses that could lead to incorrect implementations
|
|
19
|
-
-
|
|
37
|
+
- **NEVER** ask questions inline in response text - always use `AskUserQuestion` tool
|
|
38
|
+
|
|
39
|
+
### Question Policy (MANDATORY)
|
|
40
|
+
- **ALL questions MUST use `AskUserQuestion` tool** - no exceptions
|
|
41
|
+
- Never ask questions as plain text in responses
|
|
42
|
+
- This includes:
|
|
43
|
+
- Clarification questions
|
|
44
|
+
- Option/choice selection
|
|
45
|
+
- Confirmation requests
|
|
46
|
+
- Any user input needed
|
|
47
|
+
|
|
48
|
+
**Why:**
|
|
49
|
+
- Plain text questions get buried in long responses
|
|
50
|
+
- Tool-based questions provide clear UI separation
|
|
51
|
+
- Ensures user doesn't miss important decisions
|
|
20
52
|
|
|
21
53
|
### User Confirmation Required
|
|
22
54
|
Always confirm with the user before:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Code OSC Notification Script
|
|
3
|
+
# /dev/tty로 출력하여 터미널에 직접 전달
|
|
4
|
+
|
|
5
|
+
# stdin에서 JSON 읽기 (timeout으로 blocking 방지)
|
|
6
|
+
INPUT=$(timeout 1 cat 2>/dev/null || true)
|
|
7
|
+
|
|
8
|
+
# JSON 파싱
|
|
9
|
+
if command -v jq &>/dev/null && [ -n "$INPUT" ]; then
|
|
10
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
11
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
12
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null)
|
|
13
|
+
NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // empty' 2>/dev/null)
|
|
14
|
+
MESSAGE=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
|
|
15
|
+
|
|
16
|
+
FOLDER=$(basename "${CWD:-$PWD}" 2>/dev/null)
|
|
17
|
+
SHORT_ID="${SESSION_ID:0:8}"
|
|
18
|
+
|
|
19
|
+
# 제목 구성
|
|
20
|
+
TITLE="Claude"
|
|
21
|
+
[ -n "$FOLDER" ] && TITLE="$TITLE - $FOLDER"
|
|
22
|
+
[ -n "$SHORT_ID" ] && TITLE="$TITLE [$SHORT_ID]"
|
|
23
|
+
|
|
24
|
+
# 본문 구성
|
|
25
|
+
case "$EVENT" in
|
|
26
|
+
"Stop")
|
|
27
|
+
BODY="Task completed"
|
|
28
|
+
;;
|
|
29
|
+
"Notification")
|
|
30
|
+
case "$NOTIF_TYPE" in
|
|
31
|
+
"permission_prompt") BODY="Permission needed" ;;
|
|
32
|
+
"idle_prompt") BODY="Waiting for input" ;;
|
|
33
|
+
*) BODY="${MESSAGE:-$NOTIF_TYPE}" ;;
|
|
34
|
+
esac
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
BODY="${MESSAGE:-Notification}"
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
else
|
|
41
|
+
TITLE="${1:-Claude Code}"
|
|
42
|
+
BODY="${2:-Task completed}"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# OSC 777 알림 출력 (OSC 9는 중복 알림 발생하므로 제외)
|
|
46
|
+
# /dev/tty 사용 가능 시 직접 출력, 아니면 stdout
|
|
47
|
+
{
|
|
48
|
+
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY"
|
|
49
|
+
} > /dev/tty 2>/dev/null || {
|
|
50
|
+
printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exit 0
|
package/README.md
CHANGED
|
@@ -267,6 +267,27 @@ cp node_modules/@yeongjaeyou/claude-code-config/.mcp.json .
|
|
|
267
267
|
- Hugging Face model/dataset/Spaces search via `huggingface_hub` API
|
|
268
268
|
- Download and analyze source code in temp directory (`/tmp/`)
|
|
269
269
|
|
|
270
|
+
## Notification Hooks
|
|
271
|
+
|
|
272
|
+
Desktop notifications for Claude Code events using OSC escape sequences.
|
|
273
|
+
|
|
274
|
+
### Quick Setup
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# 1. Install Terminal Notification extension in VSCode
|
|
278
|
+
# 2. Global install includes notify_osc.sh and auto-registers hooks
|
|
279
|
+
npx @yeongjaeyou/claude-code-config --global
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Supported Events
|
|
283
|
+
|
|
284
|
+
| Event | Description |
|
|
285
|
+
|-------|-------------|
|
|
286
|
+
| Stop | Task completion |
|
|
287
|
+
| Notification | Permission requests, idle prompts |
|
|
288
|
+
|
|
289
|
+
See [docs/notification-setup.md](docs/notification-setup.md) for detailed setup guide.
|
|
290
|
+
|
|
270
291
|
## License
|
|
271
292
|
|
|
272
293
|
MIT License
|
package/bin/cli.js
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
-
const readline = require('readline');
|
|
7
6
|
|
|
8
7
|
const pkg = require('../package.json');
|
|
9
|
-
const
|
|
8
|
+
const {
|
|
9
|
+
INSTALL_FOLDERS,
|
|
10
|
+
askQuestion,
|
|
11
|
+
checkExistingFolders,
|
|
12
|
+
installFolders,
|
|
13
|
+
mergeSettingsJson
|
|
14
|
+
} = require('../lib/cli-utils');
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
const INSTALL_FOLDERS = ['commands', 'agents', 'skills', 'guidelines', 'hooks'];
|
|
16
|
+
const args = process.argv.slice(2);
|
|
13
17
|
|
|
14
18
|
// Parse options
|
|
15
19
|
const isGlobal = args.includes('--global') || args.includes('-g');
|
|
@@ -57,189 +61,6 @@ const dest = isGlobal
|
|
|
57
61
|
? path.join(os.homedir(), '.claude')
|
|
58
62
|
: path.join(process.cwd(), '.claude');
|
|
59
63
|
|
|
60
|
-
/**
|
|
61
|
-
* Ask user a question and get answer
|
|
62
|
-
* @param {string} question - Question content
|
|
63
|
-
* @returns {Promise<string>} - User answer
|
|
64
|
-
*/
|
|
65
|
-
function askQuestion(question) {
|
|
66
|
-
// Detect non-interactive environment (CI/CD, pipelines, etc.)
|
|
67
|
-
if (!process.stdin.isTTY) {
|
|
68
|
-
console.log('Non-interactive environment detected. Using default (update).');
|
|
69
|
-
return Promise.resolve('update');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const rl = readline.createInterface({
|
|
73
|
-
input: process.stdin,
|
|
74
|
-
output: process.stdout
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
rl.question(question, (answer) => {
|
|
79
|
-
rl.close();
|
|
80
|
-
resolve(answer.trim().toLowerCase());
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Check existing installation folders
|
|
87
|
-
* @returns {string[]} - List of existing folders
|
|
88
|
-
*/
|
|
89
|
-
function checkExistingFolders() {
|
|
90
|
-
const existing = [];
|
|
91
|
-
for (const folder of INSTALL_FOLDERS) {
|
|
92
|
-
const folderPath = path.join(dest, folder);
|
|
93
|
-
if (fs.existsSync(folderPath)) {
|
|
94
|
-
existing.push(folder);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return existing;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Copy folder recursively
|
|
102
|
-
* @param {string} src - Source path
|
|
103
|
-
* @param {string} dst - Destination path
|
|
104
|
-
* @param {string} mode - Installation mode: 'merge' | 'update'
|
|
105
|
-
*/
|
|
106
|
-
function copyFolder(src, dst, mode = 'merge') {
|
|
107
|
-
if (!fs.existsSync(src)) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Create destination folder
|
|
112
|
-
if (!fs.existsSync(dst)) {
|
|
113
|
-
try {
|
|
114
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
115
|
-
} catch (err) {
|
|
116
|
-
throw new Error(`Failed to create folder: ${dst} - ${err.message}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
121
|
-
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
const srcPath = path.join(src, entry.name);
|
|
124
|
-
const dstPath = path.join(dst, entry.name);
|
|
125
|
-
|
|
126
|
-
if (entry.isDirectory()) {
|
|
127
|
-
copyFolder(srcPath, dstPath, mode);
|
|
128
|
-
} else {
|
|
129
|
-
// In merge mode, keep existing files (skip if exists)
|
|
130
|
-
if (mode === 'merge' && fs.existsSync(dstPath)) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
// In update mode, overwrite existing files + add new files
|
|
134
|
-
// Custom files (only in dest) are preserved since we don't delete them
|
|
135
|
-
try {
|
|
136
|
-
fs.copyFileSync(srcPath, dstPath);
|
|
137
|
-
// Set execute permission for shell scripts
|
|
138
|
-
if (entry.name.endsWith('.sh')) {
|
|
139
|
-
fs.chmodSync(dstPath, 0o755);
|
|
140
|
-
}
|
|
141
|
-
} catch (err) {
|
|
142
|
-
throw new Error(`Failed to copy file: ${srcPath} -> ${dstPath} - ${err.message}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Selectively copy installation target folders
|
|
150
|
-
* @param {string} mode - 'merge' | 'update'
|
|
151
|
-
*/
|
|
152
|
-
function installFolders(mode) {
|
|
153
|
-
for (const folder of INSTALL_FOLDERS) {
|
|
154
|
-
const srcFolder = path.join(source, folder);
|
|
155
|
-
const dstFolder = path.join(dest, folder);
|
|
156
|
-
|
|
157
|
-
if (!fs.existsSync(srcFolder)) {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Both modes now preserve custom files
|
|
162
|
-
// - merge: skip existing files, add new files only
|
|
163
|
-
// - update: overwrite existing files, add new files, keep custom files
|
|
164
|
-
copyFolder(srcFolder, dstFolder, mode);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Default hook configuration for inject-guidelines
|
|
169
|
-
const DEFAULT_HOOKS_CONFIG = {
|
|
170
|
-
UserPromptSubmit: [
|
|
171
|
-
{
|
|
172
|
-
matcher: '',
|
|
173
|
-
hooks: [
|
|
174
|
-
{
|
|
175
|
-
type: 'command',
|
|
176
|
-
command: 'bash .claude/hooks/inject-guidelines.sh'
|
|
177
|
-
}
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Check if a hook with the same command already exists
|
|
185
|
-
* @param {Array} hooks - Existing hooks array
|
|
186
|
-
* @param {string} command - Command to check
|
|
187
|
-
* @returns {boolean}
|
|
188
|
-
*/
|
|
189
|
-
function hookExists(hooks, command) {
|
|
190
|
-
if (!Array.isArray(hooks)) return false;
|
|
191
|
-
return hooks.some(hook => {
|
|
192
|
-
if (!hook.hooks || !Array.isArray(hook.hooks)) return false;
|
|
193
|
-
return hook.hooks.some(h => h.command === command);
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Merge settings.json with default hooks configuration
|
|
199
|
-
* Preserves existing user settings and only adds new hooks
|
|
200
|
-
*/
|
|
201
|
-
function mergeSettingsJson() {
|
|
202
|
-
const settingsPath = path.join(dest, 'settings.json');
|
|
203
|
-
let settings = {};
|
|
204
|
-
|
|
205
|
-
// Load existing settings if exists
|
|
206
|
-
if (fs.existsSync(settingsPath)) {
|
|
207
|
-
try {
|
|
208
|
-
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
209
|
-
settings = JSON.parse(content);
|
|
210
|
-
} catch (err) {
|
|
211
|
-
console.log('Warning: Could not parse existing settings.json, creating new one.');
|
|
212
|
-
settings = {};
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Initialize hooks object if not exists
|
|
217
|
-
if (!settings.hooks) {
|
|
218
|
-
settings.hooks = {};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Initialize UserPromptSubmit array if not exists
|
|
222
|
-
if (!settings.hooks.UserPromptSubmit) {
|
|
223
|
-
settings.hooks.UserPromptSubmit = [];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Add default hooks if not already present
|
|
227
|
-
const targetCommand = DEFAULT_HOOKS_CONFIG.UserPromptSubmit[0].hooks[0].command;
|
|
228
|
-
if (!hookExists(settings.hooks.UserPromptSubmit, targetCommand)) {
|
|
229
|
-
settings.hooks.UserPromptSubmit.push(DEFAULT_HOOKS_CONFIG.UserPromptSubmit[0]);
|
|
230
|
-
console.log('Added inject-guidelines hook to settings.json');
|
|
231
|
-
} else {
|
|
232
|
-
console.log('inject-guidelines hook already exists in settings.json');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Write merged settings
|
|
236
|
-
try {
|
|
237
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
238
|
-
} catch (err) {
|
|
239
|
-
throw new Error(`Failed to write settings.json: ${err.message}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
64
|
/**
|
|
244
65
|
* Main function
|
|
245
66
|
*/
|
|
@@ -276,7 +97,7 @@ async function main() {
|
|
|
276
97
|
}
|
|
277
98
|
|
|
278
99
|
// Check existing installation folders
|
|
279
|
-
const existingFolders = checkExistingFolders();
|
|
100
|
+
const existingFolders = checkExistingFolders(dest);
|
|
280
101
|
let installMode = 'update';
|
|
281
102
|
|
|
282
103
|
if (existingFolders.length > 0) {
|
|
@@ -315,10 +136,10 @@ async function main() {
|
|
|
315
136
|
}
|
|
316
137
|
|
|
317
138
|
// Install folders
|
|
318
|
-
installFolders(installMode);
|
|
139
|
+
installFolders(source, dest, installMode);
|
|
319
140
|
|
|
320
141
|
// Merge settings.json with hooks configuration
|
|
321
|
-
mergeSettingsJson();
|
|
142
|
+
mergeSettingsJson(dest, isGlobal);
|
|
322
143
|
|
|
323
144
|
console.log('');
|
|
324
145
|
console.log('.claude/ folder installed successfully!');
|
|
@@ -337,6 +158,13 @@ async function main() {
|
|
|
337
158
|
if (isGlobal) {
|
|
338
159
|
console.log('Global installation complete.');
|
|
339
160
|
console.log('Claude Code commands are now available in all projects.');
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log('Desktop notifications enabled:');
|
|
163
|
+
console.log(' - Stop: Task completion alerts');
|
|
164
|
+
console.log(' - Notification: Permission/idle prompts');
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log('Note: Install "Terminal Notification" VSCode extension for alerts.');
|
|
167
|
+
console.log('See docs/notification-setup.md for detailed setup guide.');
|
|
340
168
|
}
|
|
341
169
|
}
|
|
342
170
|
|
package/lib/cli-utils.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Utility Functions
|
|
3
|
+
* Modularized from bin/cli.js for testability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
// Installation target folders (non-user data only)
|
|
11
|
+
const INSTALL_FOLDERS = ['commands', 'agents', 'skills', 'guidelines', 'hooks'];
|
|
12
|
+
|
|
13
|
+
// Default hook configuration for inject-guidelines
|
|
14
|
+
const DEFAULT_HOOKS_CONFIG = {
|
|
15
|
+
UserPromptSubmit: [
|
|
16
|
+
{
|
|
17
|
+
matcher: '',
|
|
18
|
+
hooks: [
|
|
19
|
+
{
|
|
20
|
+
type: 'command',
|
|
21
|
+
command: 'bash .claude/hooks/inject-guidelines.sh'
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Global-only notification hooks (OSC desktop alerts)
|
|
29
|
+
const GLOBAL_NOTIFICATION_HOOKS = {
|
|
30
|
+
Stop: [
|
|
31
|
+
{
|
|
32
|
+
hooks: [
|
|
33
|
+
{
|
|
34
|
+
type: 'command',
|
|
35
|
+
command: '~/.claude/hooks/notify_osc.sh',
|
|
36
|
+
timeout: 10
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
Notification: [
|
|
42
|
+
{
|
|
43
|
+
matcher: '',
|
|
44
|
+
hooks: [
|
|
45
|
+
{
|
|
46
|
+
type: 'command',
|
|
47
|
+
command: '~/.claude/hooks/notify_osc.sh',
|
|
48
|
+
timeout: 10
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ask user a question and get answer
|
|
57
|
+
* @param {string} question - Question content
|
|
58
|
+
* @param {Object} options - Options for readline
|
|
59
|
+
* @param {NodeJS.ReadStream} options.input - Input stream (default: process.stdin)
|
|
60
|
+
* @param {NodeJS.WriteStream} options.output - Output stream (default: process.stdout)
|
|
61
|
+
* @returns {Promise<string>} - User answer
|
|
62
|
+
*/
|
|
63
|
+
function askQuestion(question, options = {}) {
|
|
64
|
+
const input = options.input || process.stdin;
|
|
65
|
+
const output = options.output || process.stdout;
|
|
66
|
+
|
|
67
|
+
// Detect non-interactive environment (CI/CD, pipelines, etc.)
|
|
68
|
+
if (!input.isTTY) {
|
|
69
|
+
console.log('Non-interactive environment detected. Using default (update).');
|
|
70
|
+
return Promise.resolve('update');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rl = readline.createInterface({
|
|
74
|
+
input,
|
|
75
|
+
output
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
rl.question(question, (answer) => {
|
|
80
|
+
rl.close();
|
|
81
|
+
resolve(answer.trim().toLowerCase());
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check existing installation folders
|
|
88
|
+
* @param {string} dest - Destination path
|
|
89
|
+
* @returns {string[]} - List of existing folders
|
|
90
|
+
*/
|
|
91
|
+
function checkExistingFolders(dest) {
|
|
92
|
+
const existing = [];
|
|
93
|
+
for (const folder of INSTALL_FOLDERS) {
|
|
94
|
+
const folderPath = path.join(dest, folder);
|
|
95
|
+
if (fs.existsSync(folderPath)) {
|
|
96
|
+
existing.push(folder);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return existing;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Copy folder recursively
|
|
104
|
+
* @param {string} src - Source path
|
|
105
|
+
* @param {string} dst - Destination path
|
|
106
|
+
* @param {string} mode - Installation mode: 'merge' | 'update'
|
|
107
|
+
*/
|
|
108
|
+
function copyFolder(src, dst, mode = 'merge') {
|
|
109
|
+
if (!fs.existsSync(src)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create destination folder
|
|
114
|
+
if (!fs.existsSync(dst)) {
|
|
115
|
+
try {
|
|
116
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(`Failed to create folder: ${dst} - ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const srcPath = path.join(src, entry.name);
|
|
126
|
+
const dstPath = path.join(dst, entry.name);
|
|
127
|
+
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
copyFolder(srcPath, dstPath, mode);
|
|
130
|
+
} else {
|
|
131
|
+
// In merge mode, keep existing files (skip if exists)
|
|
132
|
+
if (mode === 'merge' && fs.existsSync(dstPath)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// In update mode, overwrite existing files + add new files
|
|
136
|
+
// Custom files (only in dest) are preserved since we don't delete them
|
|
137
|
+
try {
|
|
138
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
139
|
+
// Set execute permission for shell scripts
|
|
140
|
+
if (entry.name.endsWith('.sh')) {
|
|
141
|
+
fs.chmodSync(dstPath, 0o755);
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new Error(`Failed to copy file: ${srcPath} -> ${dstPath} - ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Selectively copy installation target folders
|
|
152
|
+
* @param {string} source - Source base path
|
|
153
|
+
* @param {string} dest - Destination base path
|
|
154
|
+
* @param {string} mode - 'merge' | 'update'
|
|
155
|
+
*/
|
|
156
|
+
function installFolders(source, dest, mode) {
|
|
157
|
+
for (const folder of INSTALL_FOLDERS) {
|
|
158
|
+
const srcFolder = path.join(source, folder);
|
|
159
|
+
const dstFolder = path.join(dest, folder);
|
|
160
|
+
|
|
161
|
+
if (!fs.existsSync(srcFolder)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Both modes now preserve custom files
|
|
166
|
+
// - merge: skip existing files, add new files only
|
|
167
|
+
// - update: overwrite existing files, add new files, keep custom files
|
|
168
|
+
copyFolder(srcFolder, dstFolder, mode);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a hook with the same command already exists
|
|
174
|
+
* @param {Array} hooks - Existing hooks array
|
|
175
|
+
* @param {string} command - Command to check
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function hookExists(hooks, command) {
|
|
179
|
+
if (!Array.isArray(hooks)) return false;
|
|
180
|
+
return hooks.some(hook => {
|
|
181
|
+
if (!hook.hooks || !Array.isArray(hook.hooks)) return false;
|
|
182
|
+
return hook.hooks.some(h => h.command === command);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Merge settings.json with default hooks configuration
|
|
188
|
+
* Preserves existing user settings and only adds new hooks
|
|
189
|
+
* @param {string} dest - Destination path (.claude folder)
|
|
190
|
+
* @param {boolean} isGlobal - Whether this is a global installation
|
|
191
|
+
* @param {Object} options - Optional overrides for testing
|
|
192
|
+
* @param {Object} options.defaultHooksConfig - Override DEFAULT_HOOKS_CONFIG
|
|
193
|
+
* @param {Object} options.globalNotificationHooks - Override GLOBAL_NOTIFICATION_HOOKS
|
|
194
|
+
*/
|
|
195
|
+
function mergeSettingsJson(dest, isGlobal, options = {}) {
|
|
196
|
+
const defaultHooksConfig = options.defaultHooksConfig || DEFAULT_HOOKS_CONFIG;
|
|
197
|
+
const globalNotificationHooks = options.globalNotificationHooks || GLOBAL_NOTIFICATION_HOOKS;
|
|
198
|
+
|
|
199
|
+
const settingsPath = path.join(dest, 'settings.json');
|
|
200
|
+
let settings = {};
|
|
201
|
+
|
|
202
|
+
// Load existing settings if exists
|
|
203
|
+
if (fs.existsSync(settingsPath)) {
|
|
204
|
+
try {
|
|
205
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
206
|
+
settings = JSON.parse(content);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log('Warning: Could not parse existing settings.json, creating new one.');
|
|
209
|
+
settings = {};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Initialize hooks object if not exists
|
|
214
|
+
if (!settings.hooks) {
|
|
215
|
+
settings.hooks = {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Initialize UserPromptSubmit array if not exists
|
|
219
|
+
if (!settings.hooks.UserPromptSubmit) {
|
|
220
|
+
settings.hooks.UserPromptSubmit = [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Add default hooks if not already present
|
|
224
|
+
const targetCommand = defaultHooksConfig.UserPromptSubmit[0].hooks[0].command;
|
|
225
|
+
if (!hookExists(settings.hooks.UserPromptSubmit, targetCommand)) {
|
|
226
|
+
settings.hooks.UserPromptSubmit.push(defaultHooksConfig.UserPromptSubmit[0]);
|
|
227
|
+
console.log('Added inject-guidelines hook to settings.json');
|
|
228
|
+
} else {
|
|
229
|
+
console.log('inject-guidelines hook already exists in settings.json');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Add notification hooks for global install only
|
|
233
|
+
if (isGlobal) {
|
|
234
|
+
for (const [event, hookConfigs] of Object.entries(globalNotificationHooks)) {
|
|
235
|
+
if (!settings.hooks[event]) {
|
|
236
|
+
settings.hooks[event] = [];
|
|
237
|
+
}
|
|
238
|
+
const notifCommand = hookConfigs[0].hooks[0].command;
|
|
239
|
+
if (!hookExists(settings.hooks[event], notifCommand)) {
|
|
240
|
+
settings.hooks[event].push(hookConfigs[0]);
|
|
241
|
+
console.log(`Added ${event} notification hook to settings.json`);
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`${event} notification hook already exists in settings.json`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Write merged settings
|
|
249
|
+
try {
|
|
250
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
251
|
+
} catch (err) {
|
|
252
|
+
throw new Error(`Failed to write settings.json: ${err.message}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return settings;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = {
|
|
259
|
+
// Constants
|
|
260
|
+
INSTALL_FOLDERS,
|
|
261
|
+
DEFAULT_HOOKS_CONFIG,
|
|
262
|
+
GLOBAL_NOTIFICATION_HOOKS,
|
|
263
|
+
|
|
264
|
+
// Functions
|
|
265
|
+
askQuestion,
|
|
266
|
+
checkExistingFolders,
|
|
267
|
+
copyFolder,
|
|
268
|
+
installFolders,
|
|
269
|
+
hookExists,
|
|
270
|
+
mergeSettingsJson
|
|
271
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeongjaeyou/claude-code-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Claude Code CLI custom commands, agents, and skills",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-code-config": "./bin/cli.js"
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
".claude",
|
|
10
10
|
".mcp.json",
|
|
11
|
-
"bin"
|
|
11
|
+
"bin",
|
|
12
|
+
"lib"
|
|
12
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"test:coverage": "jest --coverage"
|
|
18
|
+
},
|
|
13
19
|
"keywords": [
|
|
14
20
|
"claude-code",
|
|
15
21
|
"cli",
|
|
@@ -29,5 +35,12 @@
|
|
|
29
35
|
"homepage": "https://github.com/YoungjaeDev/claude-code-config#readme",
|
|
30
36
|
"publishConfig": {
|
|
31
37
|
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"jest": "^29.7.0",
|
|
41
|
+
"mock-fs": "^5.4.1"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=14.0.0"
|
|
32
45
|
}
|
|
33
46
|
}
|