@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
- - If any instruction/requirement is unclear, ambiguous, or potentially incorrect, use the `AskUserQuestion` tool (do not ask inline in the response text)
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
- parser = MarkdownParser(base_path=str(md_path.parent))
240
- blocks = parser.parse(content)
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 args = process.argv.slice(2);
8
+ const {
9
+ INSTALL_FOLDERS,
10
+ askQuestion,
11
+ checkExistingFolders,
12
+ installFolders,
13
+ mergeSettingsJson
14
+ } = require('../lib/cli-utils');
10
15
 
11
- // Installation target folders (non-user data only)
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!');
@@ -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.13.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
  }