@yeongjaeyou/claude-code-config 0.13.0 → 0.15.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.
|
@@ -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:
|
|
@@ -25,16 +25,22 @@ Before using this skill, ensure the following setup is complete:
|
|
|
25
25
|
|
|
26
26
|
### 2. Environment Configuration
|
|
27
27
|
|
|
28
|
-
Set the API key in the environment:
|
|
28
|
+
Set the API key in the environment. The script automatically loads `.env` files if `python-dotenv` is installed:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
# In .env file
|
|
31
|
+
# In .env file (recommended - auto-loaded)
|
|
32
32
|
NOTION_API_KEY=ntn_xxxxx
|
|
33
33
|
|
|
34
34
|
# Or export directly
|
|
35
35
|
export NOTION_API_KEY=ntn_xxxxx
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
To install dotenv support:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add python-dotenv
|
|
42
|
+
```
|
|
43
|
+
|
|
38
44
|
### 3. Page Access
|
|
39
45
|
|
|
40
46
|
Share the target parent page with the integration:
|
|
@@ -65,6 +71,8 @@ uv run python .claude/skills/notion-md-uploader/scripts/upload_md.py \
|
|
|
65
71
|
|
|
66
72
|
### Dry Run (Preview)
|
|
67
73
|
|
|
74
|
+
Preview parsing results and validate local images before uploading:
|
|
75
|
+
|
|
68
76
|
```bash
|
|
69
77
|
uv run python .claude/skills/notion-md-uploader/scripts/upload_md.py \
|
|
70
78
|
docs/analysis.md \
|
|
@@ -72,6 +80,11 @@ uv run python .claude/skills/notion-md-uploader/scripts/upload_md.py \
|
|
|
72
80
|
--dry-run
|
|
73
81
|
```
|
|
74
82
|
|
|
83
|
+
The dry run validates:
|
|
84
|
+
- Markdown block parsing
|
|
85
|
+
- Local image file existence
|
|
86
|
+
- Conversion to Notion blocks
|
|
87
|
+
|
|
75
88
|
## Supported Markdown Elements
|
|
76
89
|
|
|
77
90
|
| Element | Markdown Syntax | Notion Block |
|
|
@@ -18,10 +18,17 @@ import sys
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
|
+
# Load .env file if python-dotenv is available
|
|
22
|
+
try:
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
load_dotenv()
|
|
25
|
+
except ImportError:
|
|
26
|
+
pass # python-dotenv not installed, use env vars directly
|
|
27
|
+
|
|
21
28
|
# Add scripts directory to path for imports
|
|
22
29
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
23
30
|
|
|
24
|
-
from markdown_parser import MarkdownParser
|
|
31
|
+
from markdown_parser import BlockType, MarkdownParser
|
|
25
32
|
from notion_client import NotionClient, NotionAPIError, NotionConfig
|
|
26
33
|
from notion_converter import NotionBlockConverter
|
|
27
34
|
|
|
@@ -236,8 +243,8 @@ Setup:
|
|
|
236
243
|
print(f"Parsing: {args.md_file}")
|
|
237
244
|
content = md_path.read_text(encoding="utf-8")
|
|
238
245
|
|
|
239
|
-
|
|
240
|
-
blocks =
|
|
246
|
+
md_parser = MarkdownParser(base_path=str(md_path.parent))
|
|
247
|
+
blocks = md_parser.parse(content)
|
|
241
248
|
|
|
242
249
|
print(f"Found {len(blocks)} blocks:")
|
|
243
250
|
for i, block in enumerate(blocks[:10]):
|
|
@@ -251,6 +258,34 @@ Setup:
|
|
|
251
258
|
if len(blocks) > 10:
|
|
252
259
|
print(f" ... and {len(blocks) - 10} more blocks")
|
|
253
260
|
|
|
261
|
+
# Validate image files
|
|
262
|
+
base_path = md_path.parent
|
|
263
|
+
image_blocks = [b for b in blocks if b.block_type == BlockType.IMAGE]
|
|
264
|
+
missing_images = []
|
|
265
|
+
found_images = []
|
|
266
|
+
|
|
267
|
+
for block in image_blocks:
|
|
268
|
+
img_src = block.metadata.get("url", "")
|
|
269
|
+
if img_src and not img_src.startswith(("http://", "https://")):
|
|
270
|
+
img_path = base_path / img_src
|
|
271
|
+
if img_path.exists():
|
|
272
|
+
found_images.append(str(img_src))
|
|
273
|
+
else:
|
|
274
|
+
missing_images.append(str(img_src))
|
|
275
|
+
|
|
276
|
+
if found_images:
|
|
277
|
+
print(f"\nLocal images ({len(found_images)}):")
|
|
278
|
+
for img in found_images[:5]:
|
|
279
|
+
print(f" [OK] {img}")
|
|
280
|
+
if len(found_images) > 5:
|
|
281
|
+
print(f" ... and {len(found_images) - 5} more")
|
|
282
|
+
|
|
283
|
+
if missing_images:
|
|
284
|
+
print(f"\nMissing images ({len(missing_images)}):")
|
|
285
|
+
for img in missing_images:
|
|
286
|
+
print(f" [MISSING] {img}")
|
|
287
|
+
print("\nWarning: Missing images will cause upload to fail.")
|
|
288
|
+
|
|
254
289
|
converter = NotionBlockConverter(base_path=str(md_path.parent))
|
|
255
290
|
notion_blocks = converter.convert_blocks(blocks)
|
|
256
291
|
print(f"\nConverted to {len(notion_blocks)} Notion blocks")
|
|
@@ -272,10 +307,13 @@ Setup:
|
|
|
272
307
|
|
|
273
308
|
page_url = page.get("url", "")
|
|
274
309
|
page_id = page.get("id", "")
|
|
310
|
+
image_count = len(uploader._uploaded_images)
|
|
275
311
|
|
|
276
312
|
print(f"\nSuccess!")
|
|
277
313
|
print(f"Page ID: {page_id}")
|
|
278
314
|
print(f"URL: {page_url}")
|
|
315
|
+
if image_count > 0:
|
|
316
|
+
print(f"Images uploaded: {image_count}")
|
|
279
317
|
|
|
280
318
|
except NotionAPIError as e:
|
|
281
319
|
print(f"\nNotion API Error: {e}")
|
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,232 +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
|
-
// Global-only notification hooks (OSC desktop alerts)
|
|
184
|
-
const GLOBAL_NOTIFICATION_HOOKS = {
|
|
185
|
-
Stop: [
|
|
186
|
-
{
|
|
187
|
-
hooks: [
|
|
188
|
-
{
|
|
189
|
-
type: 'command',
|
|
190
|
-
command: '~/.claude/hooks/notify_osc.sh',
|
|
191
|
-
timeout: 10
|
|
192
|
-
}
|
|
193
|
-
]
|
|
194
|
-
}
|
|
195
|
-
],
|
|
196
|
-
Notification: [
|
|
197
|
-
{
|
|
198
|
-
matcher: '',
|
|
199
|
-
hooks: [
|
|
200
|
-
{
|
|
201
|
-
type: 'command',
|
|
202
|
-
command: '~/.claude/hooks/notify_osc.sh',
|
|
203
|
-
timeout: 10
|
|
204
|
-
}
|
|
205
|
-
]
|
|
206
|
-
}
|
|
207
|
-
]
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Check if a hook with the same command already exists
|
|
212
|
-
* @param {Array} hooks - Existing hooks array
|
|
213
|
-
* @param {string} command - Command to check
|
|
214
|
-
* @returns {boolean}
|
|
215
|
-
*/
|
|
216
|
-
function hookExists(hooks, command) {
|
|
217
|
-
if (!Array.isArray(hooks)) return false;
|
|
218
|
-
return hooks.some(hook => {
|
|
219
|
-
if (!hook.hooks || !Array.isArray(hook.hooks)) return false;
|
|
220
|
-
return hook.hooks.some(h => h.command === command);
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Merge settings.json with default hooks configuration
|
|
226
|
-
* Preserves existing user settings and only adds new hooks
|
|
227
|
-
*/
|
|
228
|
-
function mergeSettingsJson() {
|
|
229
|
-
const settingsPath = path.join(dest, 'settings.json');
|
|
230
|
-
let settings = {};
|
|
231
|
-
|
|
232
|
-
// Load existing settings if exists
|
|
233
|
-
if (fs.existsSync(settingsPath)) {
|
|
234
|
-
try {
|
|
235
|
-
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
236
|
-
settings = JSON.parse(content);
|
|
237
|
-
} catch (err) {
|
|
238
|
-
console.log('Warning: Could not parse existing settings.json, creating new one.');
|
|
239
|
-
settings = {};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Initialize hooks object if not exists
|
|
244
|
-
if (!settings.hooks) {
|
|
245
|
-
settings.hooks = {};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Initialize UserPromptSubmit array if not exists
|
|
249
|
-
if (!settings.hooks.UserPromptSubmit) {
|
|
250
|
-
settings.hooks.UserPromptSubmit = [];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Add default hooks if not already present
|
|
254
|
-
const targetCommand = DEFAULT_HOOKS_CONFIG.UserPromptSubmit[0].hooks[0].command;
|
|
255
|
-
if (!hookExists(settings.hooks.UserPromptSubmit, targetCommand)) {
|
|
256
|
-
settings.hooks.UserPromptSubmit.push(DEFAULT_HOOKS_CONFIG.UserPromptSubmit[0]);
|
|
257
|
-
console.log('Added inject-guidelines hook to settings.json');
|
|
258
|
-
} else {
|
|
259
|
-
console.log('inject-guidelines hook already exists in settings.json');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Add notification hooks for global install only
|
|
263
|
-
if (isGlobal) {
|
|
264
|
-
for (const [event, hookConfigs] of Object.entries(GLOBAL_NOTIFICATION_HOOKS)) {
|
|
265
|
-
if (!settings.hooks[event]) {
|
|
266
|
-
settings.hooks[event] = [];
|
|
267
|
-
}
|
|
268
|
-
const notifCommand = hookConfigs[0].hooks[0].command;
|
|
269
|
-
if (!hookExists(settings.hooks[event], notifCommand)) {
|
|
270
|
-
settings.hooks[event].push(hookConfigs[0]);
|
|
271
|
-
console.log(`Added ${event} notification hook to settings.json`);
|
|
272
|
-
} else {
|
|
273
|
-
console.log(`${event} notification hook already exists in settings.json`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Write merged settings
|
|
279
|
-
try {
|
|
280
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
281
|
-
} catch (err) {
|
|
282
|
-
throw new Error(`Failed to write settings.json: ${err.message}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
64
|
/**
|
|
287
65
|
* Main function
|
|
288
66
|
*/
|
|
@@ -319,7 +97,7 @@ async function main() {
|
|
|
319
97
|
}
|
|
320
98
|
|
|
321
99
|
// Check existing installation folders
|
|
322
|
-
const existingFolders = checkExistingFolders();
|
|
100
|
+
const existingFolders = checkExistingFolders(dest);
|
|
323
101
|
let installMode = 'update';
|
|
324
102
|
|
|
325
103
|
if (existingFolders.length > 0) {
|
|
@@ -358,10 +136,10 @@ async function main() {
|
|
|
358
136
|
}
|
|
359
137
|
|
|
360
138
|
// Install folders
|
|
361
|
-
installFolders(installMode);
|
|
139
|
+
installFolders(source, dest, installMode);
|
|
362
140
|
|
|
363
141
|
// Merge settings.json with hooks configuration
|
|
364
|
-
mergeSettingsJson();
|
|
142
|
+
mergeSettingsJson(dest, isGlobal);
|
|
365
143
|
|
|
366
144
|
console.log('');
|
|
367
145
|
console.log('.claude/ folder installed successfully!');
|
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.15.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
|
}
|