jun-claude-code 0.1.0 → 0.1.1

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.
@@ -149,7 +149,7 @@ function writeJson(filePath, data) {
149
149
  writeJson(path.join(sourceDir, 'settings.json'), sourceSettings);
150
150
  (0, copy_1.mergeSettingsJson)(sourceDir, destDir);
151
151
  const result = readJson(path.join(destDir, 'settings.json'));
152
- (0, vitest_1.expect)(result.statusLine).toEqual(sourceSettings.statusLine);
152
+ (0, vitest_1.expect)(result.statusLine).toBeUndefined();
153
153
  (0, vitest_1.expect)(result.hooks.UserPromptSubmit).toHaveLength(1);
154
154
  (0, vitest_1.expect)(result.hooks.UserPromptSubmit[0].hooks[0].command).toBe('skill-forced.sh');
155
155
  });
@@ -268,6 +268,73 @@ function writeJson(filePath, data) {
268
268
  const result = readJson(path.join(destDir, 'settings.json'));
269
269
  (0, vitest_1.expect)(result).toEqual(destSettings);
270
270
  });
271
+ (0, vitest_1.it)('should exclude statusLine from source even on fresh install', () => {
272
+ const sourceSettings = {
273
+ statusLine: { type: 'command', command: '~/.claude/statusline-command.sh' },
274
+ hooks: {},
275
+ };
276
+ writeJson(path.join(sourceDir, 'settings.json'), sourceSettings);
277
+ (0, copy_1.mergeSettingsJson)(sourceDir, destDir);
278
+ const result = readJson(path.join(destDir, 'settings.json'));
279
+ (0, vitest_1.expect)(result.statusLine).toBeUndefined();
280
+ });
281
+ (0, vitest_1.it)('should preserve existing statusLine in dest when source also has statusLine', () => {
282
+ const sourceSettings = {
283
+ statusLine: { type: 'command', command: 'source-status' },
284
+ hooks: {},
285
+ };
286
+ writeJson(path.join(sourceDir, 'settings.json'), sourceSettings);
287
+ const destSettings = {
288
+ statusLine: { type: 'command', command: 'dest-status' },
289
+ };
290
+ writeJson(path.join(destDir, 'settings.json'), destSettings);
291
+ (0, copy_1.mergeSettingsJson)(sourceDir, destDir);
292
+ const result = readJson(path.join(destDir, 'settings.json'));
293
+ (0, vitest_1.expect)(result.statusLine.command).toBe('dest-status');
294
+ });
295
+ (0, vitest_1.it)('should convert ~/.claude/ to ./.claude/ in command fields when project=true', () => {
296
+ const sourceSettings = {
297
+ hooks: {
298
+ UserPromptSubmit: [
299
+ {
300
+ hooks: [
301
+ { type: 'command', command: '~/.claude/hooks/skill-forced.sh' },
302
+ ],
303
+ },
304
+ ],
305
+ PreToolUse: [
306
+ {
307
+ matcher: 'Bash',
308
+ hooks: [
309
+ { type: 'command', command: '~/.claude/hooks/blocker.sh' },
310
+ ],
311
+ },
312
+ ],
313
+ },
314
+ };
315
+ writeJson(path.join(sourceDir, 'settings.json'), sourceSettings);
316
+ (0, copy_1.mergeSettingsJson)(sourceDir, destDir, { project: true });
317
+ const result = readJson(path.join(destDir, 'settings.json'));
318
+ (0, vitest_1.expect)(result.hooks.UserPromptSubmit[0].hooks[0].command).toBe('./.claude/hooks/skill-forced.sh');
319
+ (0, vitest_1.expect)(result.hooks.PreToolUse[0].hooks[0].command).toBe('./.claude/hooks/blocker.sh');
320
+ });
321
+ (0, vitest_1.it)('should keep ~/.claude/ paths unchanged when project option is not set', () => {
322
+ const sourceSettings = {
323
+ hooks: {
324
+ UserPromptSubmit: [
325
+ {
326
+ hooks: [
327
+ { type: 'command', command: '~/.claude/hooks/skill-forced.sh' },
328
+ ],
329
+ },
330
+ ],
331
+ },
332
+ };
333
+ writeJson(path.join(sourceDir, 'settings.json'), sourceSettings);
334
+ (0, copy_1.mergeSettingsJson)(sourceDir, destDir);
335
+ const result = readJson(path.join(destDir, 'settings.json'));
336
+ (0, vitest_1.expect)(result.hooks.UserPromptSubmit[0].hooks[0].command).toBe('~/.claude/hooks/skill-forced.sh');
337
+ });
271
338
  });
272
339
  // ─── init-project.ts mergeSettingsJson (project) ───
273
340
  (0, vitest_1.describe)('init-project.ts mergeSettingsJson', () => {
package/dist/copy.d.ts CHANGED
@@ -7,8 +7,12 @@ export interface CopyOptions {
7
7
  * Merge settings.json from source into destination.
8
8
  * Hooks are merged per event key; duplicate hook entries (by deep equality) are skipped.
9
9
  * Non-hook keys are shallow-merged (source wins for new keys, dest preserved for existing).
10
+ * statusLine is always excluded from merge (personal environment setting).
11
+ * When project=true, ~/.claude/ paths in command fields are converted to ./.claude/.
10
12
  */
11
- export declare function mergeSettingsJson(sourceDir: string, destDir: string): void;
13
+ export declare function mergeSettingsJson(sourceDir: string, destDir: string, options?: {
14
+ project?: boolean;
15
+ }): void;
12
16
  /**
13
17
  * Copy .claude files to user's home directory
14
18
  */
package/dist/copy.js CHANGED
@@ -125,7 +125,35 @@ function getDestClaudeDir() {
125
125
  * Hooks are merged per event key; duplicate hook entries (by deep equality) are skipped.
126
126
  * Non-hook keys are shallow-merged (source wins for new keys, dest preserved for existing).
127
127
  */
128
- function mergeSettingsJson(sourceDir, destDir) {
128
+ /**
129
+ * Replace ~/.claude/ paths with ./.claude/ in all command fields (deep traverse).
130
+ */
131
+ function replaceClaudePaths(obj) {
132
+ const result = {};
133
+ for (const [key, value] of Object.entries(obj)) {
134
+ if (key === 'command' && typeof value === 'string') {
135
+ result[key] = value.replace(/~\/\.claude\//g, './.claude/');
136
+ }
137
+ else if (Array.isArray(value)) {
138
+ result[key] = value.map((item) => typeof item === 'object' && item !== null ? replaceClaudePaths(item) : item);
139
+ }
140
+ else if (typeof value === 'object' && value !== null) {
141
+ result[key] = replaceClaudePaths(value);
142
+ }
143
+ else {
144
+ result[key] = value;
145
+ }
146
+ }
147
+ return result;
148
+ }
149
+ /**
150
+ * Merge settings.json from source into destination.
151
+ * Hooks are merged per event key; duplicate hook entries (by deep equality) are skipped.
152
+ * Non-hook keys are shallow-merged (source wins for new keys, dest preserved for existing).
153
+ * statusLine is always excluded from merge (personal environment setting).
154
+ * When project=true, ~/.claude/ paths in command fields are converted to ./.claude/.
155
+ */
156
+ function mergeSettingsJson(sourceDir, destDir, options) {
129
157
  const sourcePath = path.join(sourceDir, 'settings.json');
130
158
  const destPath = path.join(destDir, 'settings.json');
131
159
  if (!fs.existsSync(sourcePath)) {
@@ -151,8 +179,8 @@ function mergeSettingsJson(sourceDir, destDir) {
151
179
  }
152
180
  // Merge top-level keys (source fills in missing keys, dest's existing keys preserved)
153
181
  for (const key of Object.keys(sourceSettings)) {
154
- if (key === 'hooks') {
155
- continue; // hooks are merged separately below
182
+ if (key === 'hooks' || key === 'statusLine') {
183
+ continue; // hooks are merged separately below; statusLine is excluded
156
184
  }
157
185
  if (!(key in destSettings)) {
158
186
  destSettings[key] = sourceSettings[key];
@@ -180,6 +208,10 @@ function mergeSettingsJson(sourceDir, destDir) {
180
208
  }
181
209
  }
182
210
  }
211
+ // Convert ~/.claude/ → ./.claude/ paths for project mode
212
+ if (options?.project) {
213
+ destSettings = replaceClaudePaths(destSettings);
214
+ }
183
215
  ensureDir(destDir);
184
216
  fs.writeFileSync(destPath, JSON.stringify(destSettings, null, 2) + '\n', 'utf-8');
185
217
  console.log(` ${chalk_1.default.blue('[merged]')} settings.json`);
@@ -203,6 +235,7 @@ async function copyClaudeFiles(options = {}) {
203
235
  // Files to exclude from global copy (merge-handled separately)
204
236
  const EXCLUDE_FROM_GLOBAL = [
205
237
  'settings.json',
238
+ 'statusline-command.sh',
206
239
  ];
207
240
  // Get all files to copy
208
241
  const allFiles = getAllFiles(sourceDir);
@@ -271,7 +304,7 @@ async function copyClaudeFiles(options = {}) {
271
304
  copiedCount++;
272
305
  }
273
306
  // Merge settings.json (hooks are merged, not overwritten)
274
- mergeSettingsJson(sourceDir, destDir);
307
+ mergeSettingsJson(sourceDir, destDir, { project });
275
308
  console.log();
276
309
  console.log(chalk_1.default.green(`Done! Copied ${copiedCount} files, skipped ${skippedCount} files.`));
277
310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jun-claude-code",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Claude Code configuration template - copy .claude settings to your project",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/cli.js",
@@ -100,25 +100,63 @@ git log --oneline -10
100
100
  git diff main...HEAD
101
101
  ```
102
102
 
103
- ### Step 2: 변경사항 분석
103
+ ### Step 2: PR 템플릿 확인
104
+
105
+ ```bash
106
+ # 프로젝트에 PR 템플릿이 있는지 확인
107
+ # 확인 경로 (우선순위 순):
108
+ # 1. .github/pull_request_template.md
109
+ # 2. .github/PULL_REQUEST_TEMPLATE.md
110
+ # 3. docs/pull_request_template.md
111
+ # 4. pull_request_template.md
112
+ ```
113
+
114
+ - **템플릿이 있으면**: 해당 템플릿의 섹션 구조를 그대로 따라 PR 본문 작성
115
+ - **템플릿이 없으면**: 아래 Step 4의 기본 포맷 사용
116
+
117
+ ### Step 3: 변경사항 분석
104
118
 
105
119
  모든 커밋을 분석하여 PR 내용 구성:
106
120
  - 변경된 파일 목록
107
121
  - 각 파일의 변경 내용
108
122
  - 전체 변경의 목적
109
123
 
110
- ### Step 3: PR 생성
124
+ ### PR 본문 필수 포함 사항
125
+
126
+ **템플릿 사용 여부와 관계없이**, PR 본문에는 반드시 다음 3가지를 포함:
127
+
128
+ | 항목 | 설명 | 예시 |
129
+ |------|------|------|
130
+ | **의도 (Why)** | 이 변경을 하는 이유/배경 | "사용자 로그인 실패 시 에러 메시지가 표시되지 않는 문제" |
131
+ | **문제 (What)** | 해결하려는 구체적 문제 | "catch 블록에서 에러를 무시하고 있었음" |
132
+ | **해결 방법 (How)** | 어떻게 해결했는지 | "에러 핸들러를 추가하고 toast 알림으로 사용자에게 표시" |
133
+
134
+ ### Step 4: PR 생성
111
135
 
112
136
  ```bash
113
137
  # 원격에 push (필요시)
114
138
  git push -u origin <branch-name>
115
139
 
116
- # PR 생성
140
+ # PR 템플릿이 있는 경우: 템플릿 구조를 따르되 Why/What/How 포함
141
+ # PR 템플릿이 없는 경우: 아래 기본 포맷 사용
142
+
117
143
  gh pr create --title "<간결한 제목>" --body "$(cat <<'EOF'
118
144
  ## 📋 Summary
119
145
 
120
146
  > 이 PR이 해결하는 문제와 접근 방식을 1-2문장으로 설명
121
147
 
148
+ ## 🎯 Why (의도)
149
+
150
+ > 왜 이 변경이 필요한가? 배경과 동기를 설명
151
+
152
+ ## 🐛 What (문제)
153
+
154
+ > 어떤 문제가 있었는가? 구체적인 증상이나 요구사항
155
+
156
+ ## 🔧 How (해결 방법)
157
+
158
+ > 어떻게 해결했는가? 접근 방식과 핵심 변경 내용
159
+
122
160
  ## 🔄 주요 변경사항
123
161
 
124
162
  ### 변경 1: [제목]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Backend
3
- description: NestJS/TypeORM 백엔드 개발 시 사용. Controller-Service-Repository 레이어 객체 변환, find vs queryBuilder 선택 기준 제공.
4
- keywords: [Backend, 백엔드, 레이어, DTO, Entity, Service, Controller, TypeORM, find, queryBuilder]
3
+ description: NestJS/TypeORM 백엔드 개발 시 사용. 레이어 객체 변환, find vs queryBuilder 선택 기준, BDD 테스트 작성 규칙 제공.
4
+ keywords: [Backend, 백엔드, 레이어, DTO, Entity, Service, Controller, TypeORM, find, queryBuilder, test, BDD, 테스트, Jest]
5
5
  estimated_tokens: ~400
6
6
  ---
7
7
 
@@ -174,3 +174,13 @@ const user = await this.userRepository.findOneBy({ id });
174
174
  - [ ] QueryBuilder는 groupBy, getRawMany 등 필요한 경우에만 사용하는가?
175
175
 
176
176
  </checklist>
177
+
178
+ <reference>
179
+
180
+ ## 관련 문서
181
+
182
+ | 주제 | 위치 | 설명 |
183
+ |-----|------|------|
184
+ | BDD 테스트 | `bdd-testing.md` | NestJS + Jest BDD 스타일 테스트 작성 규칙 |
185
+
186
+ </reference>
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: bdd-testing
3
+ description: NestJS + Jest BDD 스타일 테스트 작성 시 사용. describe 네스팅 구조, Given/When/Then 패턴, Testing Module Mock 설정 규칙 제공.
4
+ keywords: [BDD, test, 테스트, Jest, NestJS, describe, it, Given, When, Then, mock, TestingModule]
5
+ estimated_tokens: ~500
6
+ ---
7
+
8
+ # BDD 테스트 작성 규칙
9
+
10
+ <rules>
11
+
12
+ ## describe 네스팅 구조
13
+
14
+ > **Service 클래스명 > 메서드명 > 시나리오** 구조로 describe를 네스팅한다.
15
+
16
+ | 레벨 | 대상 | 예시 |
17
+ |------|------|------|
18
+ | 1 | Service 클래스 | `describe('UserService', ...)` |
19
+ | 2 | 메서드명 | `describe('create', ...)` |
20
+ | 3 (선택) | 조건 그룹 | `describe('when role is admin', ...)` |
21
+
22
+ - 3레벨은 동일 메서드에서 조건 분기가 많을 때만 사용한다
23
+ - 3레벨까지만 사용한다
24
+
25
+ ```typescript
26
+ describe('UserService', () => {
27
+ // 공통 setup (TestingModule, mock 등)
28
+
29
+ describe('create', () => {
30
+ it('should create user when valid dto given', () => {});
31
+ it('should throw ConflictException when email duplicated', () => {});
32
+ });
33
+
34
+ describe('findById', () => {
35
+ it('should return user when id exists', () => {});
36
+ it('should throw NotFoundException when id not found', () => {});
37
+ });
38
+ });
39
+ ```
40
+
41
+ ## it 문구 및 Given/When/Then
42
+
43
+ ### it 문구 형식
44
+
45
+ ```
46
+ "should [결과] when [조건]"
47
+ ```
48
+
49
+ | 요소 | 설명 | 예시 |
50
+ |------|------|------|
51
+ | 결과 | 기대하는 동작 | `create user`, `throw NotFoundException` |
52
+ | 조건 | 입력/상태 | `valid dto given`, `id not found` |
53
+
54
+ ### 블록 내부 구조
55
+
56
+ 각 `it` 블록 내부에 `// Given` / `// When` / `// Then` 주석으로 구분한다.
57
+
58
+ ```typescript
59
+ it('should create user when valid dto given', async () => {
60
+ // Given
61
+ const dto: CreateUserDto = { name: 'John', email: 'john@test.com' };
62
+ jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(null);
63
+ jest.spyOn(userRepository, 'save').mockResolvedValue({ id: 1, ...dto } as User);
64
+
65
+ // When
66
+ const result = await service.create(dto);
67
+
68
+ // Then
69
+ expect(result.name).toBe('John');
70
+ expect(userRepository.save).toHaveBeenCalledWith(expect.objectContaining({ name: 'John' }));
71
+ });
72
+ ```
73
+
74
+ ## NestJS Testing Module 설정
75
+
76
+ ### 기본 패턴
77
+
78
+ ```typescript
79
+ import { Test, TestingModule } from '@nestjs/testing';
80
+
81
+ describe('UserService', () => {
82
+ let service: UserService;
83
+ let userRepository: Repository<User>;
84
+
85
+ beforeEach(async () => {
86
+ const module: TestingModule = await Test.createTestingModule({
87
+ providers: [
88
+ UserService,
89
+ {
90
+ provide: getRepositoryToken(User),
91
+ useValue: {
92
+ find: jest.fn(),
93
+ findOneBy: jest.fn(),
94
+ save: jest.fn(),
95
+ delete: jest.fn(),
96
+ },
97
+ },
98
+ ],
99
+ }).compile();
100
+
101
+ service = module.get<UserService>(UserService);
102
+ userRepository = module.get<Repository<User>>(getRepositoryToken(User));
103
+ });
104
+ });
105
+ ```
106
+
107
+ ### Mock 규칙
108
+
109
+ | 패턴 | 용도 | 예시 |
110
+ |------|------|------|
111
+ | `useValue` + `jest.fn()` | Repository mock | `{ save: jest.fn() }` |
112
+ | `jest.spyOn` | 특정 호출에 반환값 지정 | `jest.spyOn(repo, 'findOneBy').mockResolvedValue(...)` |
113
+ | `jest.mocked` | 이미 mock된 함수 타입 캐스팅 | `jest.mocked(repo.save).mockResolvedValue(...)` |
114
+
115
+ - `useValue`로 mock 객체를 주입하고, 각 테스트에서 `jest.spyOn` 또는 `jest.mocked`로 반환값을 지정한다
116
+ - `jest.mock()` 모듈 레벨 mock은 외부 라이브러리에만 사용한다
117
+
118
+ ### 외부 의존성 Mock
119
+
120
+ ```typescript
121
+ // Service가 다른 Service에 의존할 때
122
+ {
123
+ provide: EmailService,
124
+ useValue: {
125
+ sendWelcomeEmail: jest.fn().mockResolvedValue(undefined),
126
+ },
127
+ },
128
+ ```
129
+
130
+ </rules>
131
+
132
+ <examples>
133
+
134
+ ### describe 구조
135
+
136
+ <example type="good">
137
+ ```typescript
138
+ // Service > Method > Scenario 명확
139
+ describe('OrderService', () => {
140
+ describe('cancel', () => {
141
+ it('should cancel order when status is pending', async () => {});
142
+ it('should throw BadRequestException when status is shipped', async () => {});
143
+ });
144
+ });
145
+ ```
146
+ </example>
147
+ <example type="bad">
148
+ ```typescript
149
+ // 메서드별 그룹 없이 플랫하게 나열
150
+ describe('OrderService', () => {
151
+ it('cancel pending order', async () => {});
152
+ it('cancel shipped order throws', async () => {});
153
+ it('create order', async () => {});
154
+ });
155
+ ```
156
+ </example>
157
+
158
+ ### Assertion
159
+
160
+ <example type="good">
161
+ ```typescript
162
+ // 구체적 assertion
163
+ expect(result.status).toBe('cancelled');
164
+ expect(orderRepository.save).toHaveBeenCalledWith(
165
+ expect.objectContaining({ status: 'cancelled' }),
166
+ );
167
+ ```
168
+ </example>
169
+ <example type="bad">
170
+ ```typescript
171
+ // 의미 없는 assertion
172
+ expect(result).toBeDefined();
173
+ expect(result).toBeTruthy();
174
+ ```
175
+ </example>
176
+
177
+ ### Exception 테스트
178
+
179
+ <example type="good">
180
+ ```typescript
181
+ // 예외 타입 검증
182
+ await expect(service.cancel(orderId)).rejects.toThrow(BadRequestException);
183
+ ```
184
+ </example>
185
+ <example type="bad">
186
+ ```typescript
187
+ // try-catch로 예외 테스트
188
+ try {
189
+ await service.cancel(orderId);
190
+ fail('should have thrown');
191
+ } catch (e) {
192
+ expect(e).toBeDefined();
193
+ }
194
+ ```
195
+ </example>
196
+
197
+ </examples>
198
+
199
+ <checklist>
200
+
201
+ ## 체크리스트
202
+
203
+ - [ ] describe 네스팅이 Service > Method > Scenario 구조인가?
204
+ - [ ] it 문구가 `"should [결과] when [조건]"` 형식인가?
205
+ - [ ] 각 it 블록에 `// Given` / `// When` / `// Then` 주석이 있는가?
206
+ - [ ] `Test.createTestingModule`로 테스트 모듈을 구성했는가?
207
+ - [ ] Repository mock은 `useValue` + `jest.fn()`으로 주입했는가?
208
+ - [ ] assertion이 구체적인 값을 검증하는가?
209
+ - [ ] 예외 테스트에 `rejects.toThrow`를 사용했는가?
210
+
211
+ </checklist>
@@ -75,8 +75,12 @@ jobs:
75
75
  exit 0
76
76
  fi
77
77
 
78
+ SOURCE_PR_TITLE="${{ github.event.pull_request.title }}"
79
+ CONTEXT_PR_TITLE="${SOURCE_PR_TITLE} - Docs"
80
+ PR_NUMBER="${{ github.event.pull_request.number }}"
81
+
78
82
  CONTEXT_BRANCH="${{ github.head_ref }}-generated-context"
79
- git commit -m "docs: context 문서 자동 업데이트"
83
+ git commit -m "${CONTEXT_PR_TITLE}"
80
84
  git push --force origin HEAD:refs/heads/${CONTEXT_BRANCH}
81
85
 
82
86
  # 기존 PR이 있으면 스킵, 없으면 생성
@@ -85,11 +89,11 @@ jobs:
85
89
  gh pr create \
86
90
  --head "${CONTEXT_BRANCH}" \
87
91
  --base "${{ github.head_ref }}" \
88
- --title "docs: context 문서 자동 업데이트" \
89
- --body "$(cat <<'PREOF'
92
+ --title "${CONTEXT_PR_TITLE}" \
93
+ --body "$(cat <<PREOF
90
94
  ## Auto-generated Context
91
95
 
92
- PR의 코드 변경을 분석하여 자동 생성된 context 문서입니다.
96
+ #${PR_NUMBER} 의 코드 변경을 분석하여 자동 생성된 context 문서입니다.
93
97
  선택적으로 머지하여 반영할 수 있습니다.
94
98
 
95
99
  > 이 PR은 re-sync 시 force push로 업데이트됩니다.