@yeongjaeyou/claude-code-config 0.13.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.
@@ -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:
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.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
  }