project-auto-wizard 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -16,7 +16,7 @@ npx project-auto-wizard
16
16
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
17
17
 
18
18
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
19
- ## 최신 버전 : v0.1.9 (2026-08-02)
19
+ ## 최신 버전 : v0.1.11 (2026-08-03)
20
20
 
21
21
  [전체 버전 기록 보기](CHANGELOG.md)
22
22
 
@@ -75,26 +75,25 @@ flutter.APP_ARTIFACT_NAME:
75
75
 
76
76
  ### 완전 삭제(`--mode uninstall`)
77
77
 
78
- `npx project-auto-wizard --mode uninstall`은 `revert`보다 넓게 제거합니다 — 워크플로우·스크립트·`.coderabbit.yaml`은 물론, README.md의 `AUTO-VERSION-SECTION` 버전 섹션과 `.gitignore`에 자동 추가된 항목, `version.yml`까지 선택적으로 제거할 수 있습니다.
78
+ `npx project-auto-wizard --mode uninstall`은 `revert`보다 넓게 제거합니다 — 워크플로우·스크립트는 물론, README.md의 `AUTO-VERSION-SECTION` 버전 섹션과 `.gitignore`에 자동 추가된 항목, `version.yml`까지 선택적으로 제거할 수 있습니다.
79
79
 
80
- - **대화형(TTY)**: 실제로 설치된 항목만 체크리스트로 보여줍니다. 워크플로우/스크립트/`.coderabbit.yaml`은 기본 체크, README·`.gitignore`·`version.yml`은 opt-in입니다. 선택 후 최종 확인(기본 "아니오")을 거쳐야 실제로 삭제됩니다.
81
- - **비대화형(`--force`)**: 워크플로우·스크립트·`.coderabbit.yaml`만 기본 삭제합니다. README·`.gitignore`·`version.yml`까지 지우려면 `--purge-readme`/`--purge-gitignore`/`--purge-version`을 함께 지정하세요.
80
+ - **대화형(TTY)**: 실제로 설치된 항목만 체크리스트로 보여줍니다. 워크플로우·스크립트는 기본 체크, README·`.gitignore`·`version.yml`은 opt-in입니다. 선택 후 최종 확인(기본 "아니오")을 거쳐야 실제로 삭제됩니다.
81
+ - **비대화형(`--force`)**: 워크플로우·스크립트만 기본 삭제합니다. README·`.gitignore`·`version.yml`까지 지우려면 `--purge-readme`/`--purge-gitignore`/`--purge-version`을 함께 지정하세요.
82
82
  - `--dry-run`과 함께 쓰면 무엇이 지워질지 미리 볼 수 있습니다.
83
83
 
84
84
  ```bash
85
85
  npx project-auto-wizard --mode uninstall # 대화형 체크리스트
86
- npx project-auto-wizard --mode uninstall --force # 워크플로우·스크립트·coderabbit만 안전 삭제
86
+ npx project-auto-wizard --mode uninstall --force # 워크플로우·스크립트만 안전 삭제
87
87
  npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version # 완전 삭제
88
88
  ```
89
89
 
90
90
  ## API 키 0개 AI — 요약 엔진 체인
91
91
 
92
- 릴리스 노트는 4단 엔진 체인으로 생성됩니다. **어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
92
+ 릴리스 노트는 3단 엔진 체인으로 생성됩니다. **상용 서드파티 서비스에 전혀 의존하지 않으며, 어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
93
93
 
94
94
  ```mermaid
95
95
  flowchart LR
96
- A["CodeRabbit PR 요약<br/>(opt-in, 30s×10 폴링)"] -->|"미도착/미사용"| B["사용자 지정 AI<br/>(AI_API_KEY)"]
97
- B -->|"키 없음/실패"| C["GitHub Models<br/>(GITHUB_TOKEN만, 무료)"]
96
+ B["사용자 지정 AI<br/>(AI_API_KEY)"] -->|"키 없음/실패"| C["GitHub Models<br/>(GITHUB_TOKEN만, 무료)"]
98
97
  C -->|"rate limit/실패"| D["규칙 기반 fallback<br/>(항상 성공)"]
99
98
  ```
100
99
 
@@ -133,7 +132,6 @@ npx project-auto-wizard [옵션]
133
132
  --develop-branch B 개발 브랜치 (기본: develop)
134
133
  --nexus Nexus 라이브러리 publish 워크플로우 포함
135
134
  --secret-backup Secret 서버 백업 워크플로우 포함
136
- --coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로
137
135
  --semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함, --no-semver-auto로 끔)
138
136
  --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌
139
137
  --purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
@@ -171,7 +169,7 @@ npx project-auto-wizard --no-semver-auto # 항상 patch+1 (레거시 동작)
171
169
 
172
170
  ### 자체 AI PR 요약봇
173
171
 
174
- CodeRabbit을 쓰지 않는 레포를 위한 대안입니다. `--coderabbit`을 켜지 않으면(기본값) 릴리스 브랜치(`--main-branch`)를 대상으로 하는 PR이 열릴 때 API 키 0개 AI 엔진 체인으로 요약 코멘트를 자동으로 답니다. `--coderabbit`을 켜면 CodeRabbit이 PR 요약을 전담하고 이 봇은 no-op으로 빠집니다(중복 방지, 상호 배타적).
172
+ 상용 PR 리뷰 SaaS 없이 동작하는 자체 요약봇입니다. 릴리스 브랜치(`--main-branch`)를 대상으로 하는 PR이 열릴 때 API 키 0개 AI 엔진 체인으로 요약 코멘트를 자동으로 답니다.
175
173
 
176
174
  기본 설치(pr-flow) 기준으로 일상적인 기능 PR은 `develop`을 대상으로 열리므로, 이 봇은 develop→main 릴리스 PR에서만 실제로 동작합니다 — 해당 PR에서는 `AUTO-CHANGELOG-CONTROL`이 이미 같은 엔진으로 체인지로그 요약을 생성하므로, 이 봇은 그 요약을 PR 코멘트 형태로도 남겨주는 보조 역할입니다. 릴리스 브랜치 = 개발 브랜치인 trunk-based 모드에서는 모든 PR이 곧 릴리스 대상 브랜치를 향하므로 매 PR마다 동작합니다.
177
175
 
@@ -207,7 +205,7 @@ flowchart TB
207
205
  ## 개발
208
206
 
209
207
  ```bash
210
- npm test # node --test + python unittest (py 51 + node 59)
208
+ npm test # node --test + python unittest (node 219 + py 87)
211
209
  npm run test:node
212
210
  npm run test:py
213
211
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native AI Release Automation into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,7 +5,7 @@ changelog_manager.py
5
5
  통합 체인지로그 매니저 스크립트.
6
6
 
7
7
  서브커맨드:
8
- - update-from-summary: CodeRabbit Summary Markdown을 파싱하여 CHANGELOG.json 갱신
8
+ - update-from-summary: 릴리즈 요약 Markdown을 파싱하여 CHANGELOG.json 갱신
9
9
  - generate-md : CHANGELOG.json을 기반으로 CHANGELOG.md 재생성
10
10
  - export : 특정 버전의 릴리즈 노트를 생성하여 stdout 또는 파일로 저장
11
11
  - ai-summary : 커밋 목록으로부터 AI(또는 규칙 기반 폴백) 릴리즈 요약 생성
@@ -46,9 +46,8 @@ def _clean_summary_noise(text: str) -> str:
46
46
 
47
47
  제거 대상:
48
48
  1. HTML 주석 (<!-- ... -->)
49
- 2. CodeRabbit Tip 메시지
50
- 3. 남은 HTML 태그
51
- 4. 연속된 빈 줄
49
+ 2. 남은 HTML 태그
50
+ 3. 연속된
52
51
  """
53
52
  if not text:
54
53
  return text
@@ -56,15 +55,10 @@ def _clean_summary_noise(text: str) -> str:
56
55
  # 1. HTML 주석 제거
57
56
  text = re.sub(r'<!--.*?-->', '', text, flags=re.DOTALL)
58
57
 
59
- # 2. CodeRabbit Tip 제거
60
- text = re.sub(r'^.*?✏️\s*Tip:.*$', '', text, flags=re.MULTILINE)
61
- text = re.sub(r'<sub>.*?Tip:.*?</sub>', '', text, flags=re.IGNORECASE | re.DOTALL)
62
- text = re.sub(r'^\s*Tip:.*$', '', text, flags=re.MULTILINE | re.IGNORECASE)
63
-
64
- # 3. 남은 HTML 태그 제거
58
+ # 2. 남은 HTML 태그 제거
65
59
  text = re.sub(r'<[^>]+>', '', text)
66
60
 
67
- # 4. 연속된 빈 줄 정리 (3개 이상 → 2개)
61
+ # 3. 연속된 빈 줄 정리 (3개 이상 → 2개)
68
62
  text = re.sub(r'\n{3,}', '\n\n', text)
69
63
 
70
64
  return text.strip()
@@ -80,27 +74,27 @@ def _make_safe_key(title: str, idx: int) -> str:
80
74
 
81
75
  def _parse_summary_markdown(md_content: str) -> dict:
82
76
  """
83
- Markdown 형식의 CodeRabbit Summary 파싱.
77
+ 릴리즈 요약 Markdown을 카테고리/항목으로 파싱.
84
78
 
85
79
  3단계 폴백 전략:
86
- 1. 정밀 파싱 (현재 CodeRabbit 형식)
87
- 2. 관대한 파싱 (형식 변형 대응)
80
+ 1. 섹션 파싱 (AI 엔진 체인·규칙 기반 폴백이 실제로 생성하는 형식)
81
+ 2. 관대한 파싱 (중첩 불릿 등 형식 변형 대응)
88
82
  3. 휴리스틱 파싱 (최후 수단)
89
83
 
90
- 예상 형식:
91
- ## Summary by CodeRabbit
84
+ 예상 형식 (_build_ai_prompt / render_fallback_md가 지정하는 형식):
85
+ ## [1.2.3]
92
86
 
93
- * **버그 수정**
94
- * OCR 입력 처리 개선
95
- * 콘텐츠 응답 오류 감지 강화
87
+ ### 기능
88
+ - 사용자 로그인 추가
89
+ - 대시보드 위젯 추가
96
90
 
97
- * **Chores**
98
- * 버전 0.1.39로 업그레이드
91
+ ### 🐛 수정
92
+ - 포인터 예외 수정
99
93
  """
100
- # 1단계: 정밀 파싱
101
- detected = _parse_markdown_precise(md_content)
94
+ # 1단계: 섹션 파싱
95
+ detected = _parse_markdown_sections(md_content)
102
96
  if detected:
103
- print(" → 정밀 파서 성공")
97
+ print(" → 섹션 파서 성공")
104
98
  return detected
105
99
 
106
100
  # 2단계: 관대한 파싱
@@ -116,33 +110,51 @@ def _parse_summary_markdown(md_content: str) -> dict:
116
110
  return detected
117
111
 
118
112
 
119
- def _parse_markdown_precise(md_content: str) -> dict:
113
+ # `## [1.2.3]` / `## v1.2.3` 처럼 카테고리가 아니라 릴리즈 버전을 가리키는 헤더
114
+ _VERSION_HEADING_RE = re.compile(r'^\[?\s*v?\d+(?:\.\d+)*\s*\]?$')
115
+
116
+ _HEADING_RE = re.compile(r'^\s{0,3}(#{2,6})\s+(.+?)\s*#*\s*$')
117
+ _BULLET_RE = re.compile(r'^\s*[\*\-\+]\s+(.+?)\s*$')
118
+
119
+
120
+ def _parse_markdown_sections(md_content: str) -> dict:
120
121
  """
121
- 정밀 파서: 현재 CodeRabbit 형식에 최적화.
122
+ 섹션 파서: `### 카테고리` 헤딩 + `- 항목` 불릿 형식.
122
123
 
123
- 형식: * **카테고리**\n * 항목
124
+ AI 엔진 체인(사용자 지정 API → GitHub Models)과 규칙 기반 폴백이
125
+ 동일하게 생성하는 형식이므로 1순위로 시도한다.
124
126
  """
125
127
  detected: dict[str, dict] = {}
128
+ order: list[str] = []
129
+ current_key = None
126
130
 
127
- # 패턴: * **카테고리** (bold, 들여쓰기 2칸)
128
- pattern = r'\*\s*\*\*(.+?)\*\*\s*\n((?:\s{2}\*\s+.+(?:\n|$))*)'
129
- matches = re.findall(pattern, md_content, re.MULTILINE)
130
-
131
- for idx, (category_title, items_text) in enumerate(matches):
132
- category_title = category_title.strip()
133
-
134
- # 항목 추출: " * 항목 내용"
135
- items = re.findall(r'\s{2}\*\s+(.+)', items_text)
136
- items = [item.strip() for item in items if item.strip()]
131
+ for line in md_content.split('\n'):
132
+ heading = _HEADING_RE.match(line)
133
+ if heading:
134
+ title = heading.group(2).strip()
135
+ # 버전 헤더(`## [1.2.3]`) 카테고리가 아니다
136
+ if not title or _VERSION_HEADING_RE.match(title):
137
+ current_key = None
138
+ continue
139
+ key = _make_safe_key(title, len(order))
140
+ if key not in detected:
141
+ detected[key] = {'title': title, 'items': []}
142
+ order.append(key)
143
+ current_key = key
144
+ continue
137
145
 
138
- if not category_title and not items:
146
+ if current_key is None:
139
147
  continue
140
148
 
141
- safe_key = _make_safe_key(category_title, idx)
142
- detected[safe_key] = {
143
- 'title': category_title,
144
- 'items': items,
145
- }
149
+ bullet = _BULLET_RE.match(line)
150
+ if bullet:
151
+ item = bullet.group(1).strip()
152
+ if item:
153
+ detected[current_key]['items'].append(item)
154
+
155
+ # 헤딩만 있고 항목이 하나도 없으면 이 형식이 아니라고 보고 다음 파서로 넘긴다
156
+ if not any(entry['items'] for entry in detected.values()):
157
+ return {}
146
158
 
147
159
  return detected
148
160
 
@@ -46,6 +46,5 @@ metadata:
46
46
  options:
47
47
  nexus: {{OPT_NEXUS}}
48
48
  secret_backup: {{OPT_SECRET_BACKUP}}
49
- coderabbit: {{OPT_CODERABBIT}}
50
49
  semver_auto: {{OPT_SEMVER_AUTO}}
51
50
  {{DEPLOY}}
@@ -1,6 +1,6 @@
1
1
  # ===================================================================
2
2
  # PROJECT-COMMON-AI-PR-SUMMARY.yaml
3
- # Self-hosted AI PR summary bot — CodeRabbit-independent alternative.
3
+ # Self-hosted AI PR summary bot — no third-party service required.
4
4
  # ===================================================================
5
5
  #
6
6
  # Posts an AI-generated summary comment on PRs targeting the default
@@ -8,11 +8,6 @@
8
8
  # changelog (user API key -> GitHub Models -> rule-based fallback).
9
9
  # The prompt includes commit subjects plus a capped `git diff --stat`.
10
10
  #
11
- # Mutually exclusive with CodeRabbit by design: if version.yml
12
- # metadata.template.options.coderabbit is true, this workflow no-ops
13
- # immediately — CodeRabbit already covers PR summaries, and posting
14
- # both on the same PR would be redundant/confusing.
15
- #
16
11
  # Independent from PROJECT-COMMON-AUTO-CHANGELOG-CONTROL: on release
17
12
  # PRs both workflows may call the AI engine once each, and when
18
13
  # semver_auto is on, AUTO-CHANGELOG-CONTROL's bump classification may
@@ -46,14 +41,7 @@ jobs:
46
41
  fetch-depth: 0
47
42
  ref: ${{ github.event.pull_request.head.ref }}
48
43
 
49
- - name: Read coderabbit option from version.yml
50
- id: options
51
- run: |
52
- CODERABBIT=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?coderabbit:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
53
- echo "coderabbit=$CODERABBIT" >> $GITHUB_OUTPUT
54
-
55
44
  - name: Generate and post AI summary
56
- if: steps.options.outputs.coderabbit != 'true'
57
45
  env:
58
46
  PR_TITLE: ${{ github.event.pull_request.title }}
59
47
  AI_API_KEY: ${{ secrets.AI_API_KEY }}
@@ -10,13 +10,9 @@
10
10
  # else patch; AI-assisted patch->minor upgrade for unclassified
11
11
  # commits) and bumps accordingly.
12
12
  # - Otherwise: always patch+1 (legacy behavior).
13
- # 2. Acquires a release summary:
14
- # - If version.yml metadata.template.options.coderabbit is true:
15
- # requests a CodeRabbit summary comment once and polls the PR body
16
- # (30s x 10 polls = 5 minutes max)
17
- # - Otherwise (or on poll timeout): generates the summary locally with
18
- # changelog_manager.py ai-summary (engine chain: user API key ->
19
- # GitHub Models -> rule-based fallback; always succeeds)
13
+ # 2. Generates the release summary locally with changelog_manager.py
14
+ # ai-summary (engine chain: user API key -> GitHub Models -> rule-based
15
+ # fallback; always succeeds, no third-party service involved)
20
16
  # - The AI prompt includes commit subjects plus a capped
21
17
  # `git diff --stat` (file-level change summary, no diff body)
22
18
  # for richer context without token-cost blowup.
@@ -80,15 +76,6 @@ jobs:
80
76
  git config --local user.email "action@github.com"
81
77
  git config --local user.name "GitHub Action"
82
78
 
83
- - name: Read coderabbit option from version.yml
84
- id: options
85
- run: |
86
- # scoped to the metadata: -> template: -> options: chain so an
87
- # unrelated "coderabbit:" key can never hijack the value
88
- CODERABBIT=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?coderabbit:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
89
- echo "coderabbit=$CODERABBIT" >> $GITHUB_OUTPUT
90
- echo "coderabbit option: $CODERABBIT"
91
-
92
79
  - name: Read semver_auto option from version.yml
93
80
  id: semver_options
94
81
  run: |
@@ -110,87 +97,6 @@ jobs:
110
97
  rm -f _full_diff_stat.txt
111
98
  echo "commits to summarize: $(wc -l < commits.txt)"
112
99
 
113
- - name: Request CodeRabbit summary (PR-body-init protection)
114
- id: coderabbit_request
115
- if: steps.options.outputs.coderabbit == 'true'
116
- run: |
117
- PR_NUMBER=${{ github.event.pull_request.number }}
118
-
119
- # The PR body can lag right after open (API replication) —
120
- # retry the read up to 3 times before deciding.
121
- CURRENT_BODY=""
122
- for attempt in 1 2 3; do
123
- CURRENT_BODY=$(curl -s -H "Authorization: token ${{ github.token }}" \
124
- "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
125
- jq -r '.body // ""')
126
- if [ -n "$CURRENT_BODY" ] && [ "$CURRENT_BODY" != "null" ]; then
127
- echo "[try $attempt] body length=$(printf '%s' "$CURRENT_BODY" | wc -c)"
128
- break
129
- fi
130
- echo "[try $attempt] body empty — possible replication lag, retrying"
131
- sleep 2
132
- done
133
-
134
- if printf '%s\n' "$CURRENT_BODY" | grep -q "Summary by CodeRabbit"; then
135
- # Protection: a summary already exists — never wipe it, never re-request
136
- echo "Summary by CodeRabbit already present — skipping request"
137
- echo "already_found=true" >> $GITHUB_OUTPUT
138
- else
139
- echo "already_found=false" >> $GITHUB_OUTPUT
140
- echo "requesting CodeRabbit summary on PR #$PR_NUMBER"
141
- curl -s -H "Authorization: token ${{ github.token }}" \
142
- -H "Content-Type: application/json" \
143
- -X POST \
144
- -d '{"body": "@coderabbitai summary"}' \
145
- "https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" > /dev/null
146
- echo "CodeRabbit summary requested"
147
- fi
148
-
149
- - name: Poll PR body for CodeRabbit summary (30s x 10 polls = 5 minutes max)
150
- id: poll
151
- if: steps.options.outputs.coderabbit == 'true'
152
- run: |
153
- PR_NUMBER=${{ github.event.pull_request.number }}
154
-
155
- if [ "${{ steps.coderabbit_request.outputs.already_found }}" = "true" ]; then
156
- echo "summary already present — skipping poll"
157
- curl -s -H "Authorization: token ${{ github.token }}" \
158
- "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
159
- jq -r '.body // ""' > pr_body.md
160
- echo "summary_found=true" >> $GITHUB_OUTPUT
161
- exit 0
162
- fi
163
-
164
- MAX_POLLS=10
165
- POLL_INTERVAL=30
166
- FOUND=false
167
-
168
- for i in $(seq 1 $MAX_POLLS); do
169
- echo "[$i/$MAX_POLLS] checking PR body... ($(date '+%H:%M:%S'))"
170
-
171
- PR_BODY=$(curl -s -H "Authorization: token ${{ github.token }}" \
172
- "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
173
- jq -r '.body // ""')
174
-
175
- if [ -n "$PR_BODY" ] && [ "$PR_BODY" != "null" ] && printf '%s\n' "$PR_BODY" | grep -q "Summary by CodeRabbit"; then
176
- echo "CodeRabbit summary found"
177
- printf '%s\n' "$PR_BODY" > pr_body.md
178
- FOUND=true
179
- break
180
- fi
181
-
182
- if [ "$i" -lt "$MAX_POLLS" ]; then
183
- sleep $POLL_INTERVAL
184
- fi
185
- done
186
-
187
- if [ "$FOUND" = "true" ]; then
188
- echo "summary_found=true" >> $GITHUB_OUTPUT
189
- else
190
- echo "no CodeRabbit summary after 5 minutes — falling back to ai-summary"
191
- echo "summary_found=false" >> $GITHUB_OUTPUT
192
- fi
193
-
194
100
  - name: Confirm release version (bump + sync)
195
101
  id: bump
196
102
  env:
@@ -223,8 +129,7 @@ jobs:
223
129
  fi
224
130
  echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
225
131
 
226
- - name: Generate summary with the AI engine chain (fallback path)
227
- if: steps.options.outputs.coderabbit != 'true' || steps.poll.outputs.summary_found != 'true'
132
+ - name: Generate summary with the AI engine chain
228
133
  env:
229
134
  # PR title goes through the env block — never inline-interpolate
230
135
  # untrusted text into the shell string (quote injection).
@@ -243,8 +148,7 @@ jobs:
243
148
  --pr-title "$PR_TITLE" \
244
149
  --diff-stat-file diff_stat.txt
245
150
 
246
- # Feed the generated summary through the same channel the
247
- # CodeRabbit path uses: update-from-summary reads ./pr_body.md
151
+ # update-from-summary reads ./pr_body.md
248
152
  cp summary.md pr_body.md
249
153
 
250
154
  - name: Update CHANGELOG from summary
package/src/cli/args.js CHANGED
@@ -10,7 +10,6 @@ export function parseArgs(argv) {
10
10
  primaryType: "",
11
11
  includeNexus: null, // null=미설정
12
12
  includeSecretBackup: null,
13
- includeCodeRabbit: null, // --coderabbit / --no-coderabbit (기본 false — DESIGN-SPEC §4 질문②)
14
13
  includeSemverAuto: null, // --semver-auto / --no-semver-auto (기본 true — 미지정 시 다운스트림에서 해석)
15
14
  pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
16
15
  mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
@@ -31,7 +30,6 @@ export function parseArgs(argv) {
31
30
  keepChangelog: false,
32
31
  keepWorkflows: false,
33
32
  keepScripts: false,
34
- keepCoderabbit: false,
35
33
  };
36
34
  const args = [...argv];
37
35
  while (args.length > 0) {
@@ -76,13 +74,10 @@ export function parseArgs(argv) {
76
74
  case "--keep-changelog": result.keepChangelog = true; break;
77
75
  case "--keep-workflows": result.keepWorkflows = true; break;
78
76
  case "--keep-scripts": result.keepScripts = true; break;
79
- case "--keep-coderabbit": result.keepCoderabbit = true; break;
80
77
  case "--nexus": result.includeNexus = true; break;
81
78
  case "--no-nexus": result.includeNexus = false; break;
82
79
  case "--secret-backup": result.includeSecretBackup = true; break;
83
80
  case "--no-secret-backup": result.includeSecretBackup = false; break;
84
- case "--coderabbit": result.includeCodeRabbit = true; break;
85
- case "--no-coderabbit": result.includeCodeRabbit = false; break;
86
81
  case "--semver-auto": result.includeSemverAuto = true; break;
87
82
  case "--no-semver-auto": result.includeSemverAuto = false; break;
88
83
  case "--paths": result.pathsCsv = args.shift() ?? ""; break;
package/src/cli/help.js CHANGED
@@ -18,7 +18,6 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
18
18
  --develop-branch B 개발 브랜치 (기본: develop). 릴리스 브랜치와 같으면 trunk-based 모드
19
19
  --nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
20
20
  --secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
21
- --coderabbit / --no-coderabbit CodeRabbit PR 요약을 릴리스 노트 1순위로 사용 (기본: 사용 안 함)
22
21
  --semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
23
22
  --force 모든 확인 생략, 비대화형 기본값 사용
24
23
  --dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/version/workflows/revert/uninstall 전체 지원)
@@ -13,7 +13,7 @@ import { markerForType } from "../core/detect.js";
13
13
  function versionYmlPreview(context, payloadRoot, targetRoot) {
14
14
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
15
15
  now, today, templateVersion = "unknown",
16
- includeNexus = false, includeSecretBackup = false, includeCodeRabbit = false,
16
+ includeNexus = false, includeSecretBackup = false,
17
17
  includeSemverAuto } = context;
18
18
  const pathMarkers = new Map();
19
19
  for (const [t] of paths) pathMarkers.set(t, markerForType(t));
@@ -22,7 +22,6 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
22
22
  version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
23
23
  templateOptions: {
24
24
  templateVersion, includeNexus, includeSecretBackup,
25
- includeCodeRabbit: includeCodeRabbit === true,
26
25
  includeSemverAuto: includeSemverAuto !== false,
27
26
  optionsDate: today,
28
27
  },
@@ -57,14 +56,12 @@ export function printDryRun(plan) {
57
56
  for (const f of r.workflows) lines.push(` - ${f}`);
58
57
  lines.push(`제거될 스크립트 (${r.scripts.length}개):`);
59
58
  for (const f of r.scripts) lines.push(` - ${f}`);
60
- if (r.coderabbit) lines.push("제거될 파일: .coderabbit.yaml");
61
59
  } else if (plan.mode === "uninstall") {
62
60
  const u = plan.uninstall;
63
61
  lines.push(`제거될 워크플로우 (${u.workflows.length}개):`);
64
62
  for (const f of u.workflows) lines.push(` - ${f}`);
65
63
  lines.push(`제거될 스크립트 (${u.scripts.length}개):`);
66
64
  for (const f of u.scripts) lines.push(` - ${f}`);
67
- if (u.coderabbit) lines.push("제거될 파일: .coderabbit.yaml");
68
65
  if (u.readme) lines.push("제거될 항목: README.md 버전 섹션 (AUTO-VERSION-SECTION)");
69
66
  if (u.gitignore) lines.push("제거될 항목: .gitignore 자동 추가 항목");
70
67
  if (u.versionYml) lines.push("제거될 파일: version.yml");
@@ -1,5 +1,6 @@
1
1
  // full 모드 오케스트레이터 (.sh execute_integration full case 등가).
2
- // 복사 순서: workflows(+env 치환) → version.yml → readme → scripts → coderabbit → gitignore
2
+ // 복사 순서: workflows(+env 치환) → version.yml → readme → scripts → gitignore(조건부)
3
+ // gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다 — issue #7.
3
4
  // (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
4
5
  import { join } from "node:path";
5
6
  import { writeText } from "../core/fsutil.js";
@@ -10,7 +11,6 @@ import { markerForType } from "../core/detect.js";
10
11
  import { addVersionSectionToReadme } from "../core/copy/readme.js";
11
12
  import { copyWorkflows } from "../core/copy/workflows.js";
12
13
  import { copyScripts } from "../core/copy/simple.js";
13
- import { copyCoderabbit } from "../core/copy/coderabbit.js";
14
14
  import { ensureGitignore } from "../core/copy/gitignore.js";
15
15
 
16
16
  // context: { version, types, paths:Map, branch, versionCode, includeNexus, includeSecretBackup,
@@ -19,7 +19,7 @@ import { ensureGitignore } from "../core/copy/gitignore.js";
19
19
  export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
20
20
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
21
21
  force = true, now, today, templateVersion = "unknown",
22
- includeNexus = false, includeSecretBackup = false, includeCodeRabbit = false,
22
+ includeNexus = false, includeSecretBackup = false,
23
23
  includeSemverAuto } = context;
24
24
 
25
25
  // project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
@@ -37,7 +37,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
37
37
  templateText: readVersionYmlTemplate(payloadRoot),
38
38
  version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
39
39
  deployValues,
40
- templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeCodeRabbit: includeCodeRabbit === true, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
40
+ templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
41
41
  }));
42
42
 
43
43
  // 3. README 버전 섹션
@@ -46,9 +46,10 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
46
46
  // 4. scripts (payload/scripts/*.py → .github/scripts/)
47
47
  copyScripts(payloadRoot, targetRoot);
48
48
 
49
- // 5. coderabbit (opt-in true일 때만 DESIGN-SPEC §4 질문②) / gitignore
50
- if (includeCodeRabbit === true) copyCoderabbit(payloadRoot, { force }, targetRoot);
51
- ensureGitignore(targetRoot);
49
+ // 5. gitignore 워크플로우 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
50
+ // 충돌 없는 설치(대부분의 최초 설치) .gitignore를 전혀 건드리지 않는다 — issue #7.
51
+ const gitignoreUpdated = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0;
52
+ if (gitignoreUpdated) ensureGitignore(targetRoot);
52
53
 
53
- return { workflows: wfCounters };
54
+ return { workflows: wfCounters, gitignoreUpdated };
54
55
  }
@@ -49,7 +49,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
49
49
  const ok = await io.askYesNo("마법사가 설치한 워크플로우·스크립트를 제거할까요? (version.yml·README는 보존)", false);
50
50
  if (ok !== true) { io.cancelMessage?.("되돌리기를 취소했습니다."); return 0; }
51
51
  const r = runRevert({}, payload, cwd);
52
- io.note?.(`워크플로우 ${r.workflows.length}개, 스크립트 ${r.scripts.length}개 제거${r.coderabbit ? " + .coderabbit.yaml" : ""}`, "되돌리기 완료");
52
+ io.note?.(`워크플로우 ${r.workflows.length}개, 스크립트 ${r.scripts.length}개 제거`, "되돌리기 완료");
53
53
  io.outro?.("되돌리기를 마쳤습니다.");
54
54
  return 0;
55
55
  }
@@ -78,7 +78,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
78
78
  // 선택 워크플로우 초기값: version.yml 저장 옵션 (.sh read_template_options L2361 등가)
79
79
  let includeNexus = existing?.options?.nexus ?? false;
80
80
  let includeSecretBackup = existing?.options?.secretBackup ?? false;
81
- let includeCodeRabbit = existing?.options?.coderabbit ?? null;
82
81
  let includeSemverAuto = existing?.options?.semverAuto ?? null;
83
82
  const showOptional = mode === "full" || mode === "workflows";
84
83
  const realTty = process.stdout.isTTY === true;
@@ -97,12 +96,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
97
96
  includeNexus = r.nexus;
98
97
  includeSecretBackup = r.secretBackup;
99
98
 
100
- // 신규 질문 ② — CodeRabbit opt-in (DESIGN-SPEC §4, 기본 아니오). 저장값 있으면 재질문 생략.
101
- if (includeCodeRabbit === null) {
102
- const y = await io.askYesNo("CodeRabbit을 사용합니까? (PR AI 리뷰·요약을 릴리스 노트 1순위로 사용)", false);
103
- includeCodeRabbit = y === true;
104
- }
105
-
106
99
  // 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
107
100
  // version.yml을 쓰지 않는 workflows 모드에서는 답변이 무의미하므로 full에서만 질문한다.
108
101
  if (mode === "full" && includeSemverAuto === null) {
@@ -110,7 +103,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
110
103
  includeSemverAuto = y2 === true;
111
104
  }
112
105
  }
113
- includeCodeRabbit = includeCodeRabbit === true;
114
106
  // 질문이 실제로 나온 경우(위 full 모드 질문) 답변을 그대로 존중.
115
107
  // 질문이 안 나온 경우(version/workflows 모드) — 기존 설치는 안전하게 false로 폴백,
116
108
  // 완전 신규 설치만 true(기존 설계) 유지 — CLI 경로(index.js)와 동일한 안전 정책.
@@ -209,7 +201,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
209
201
  const { now, today } = clock || utcNow();
210
202
  const ctx = createContext({
211
203
  mode, force: true, types, version, versionCode, branch, branches, paths,
212
- includeNexus, includeSecretBackup, includeCodeRabbit,
204
+ includeNexus, includeSecretBackup,
213
205
  includeSemverAuto,
214
206
  repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
215
207
  });
@@ -247,8 +239,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
247
239
 
248
240
  // 완료 요약 (.sh print_summary L5438)
249
241
  io.summary?.({
250
- mode, types, version, branches, includeCodeRabbit,
242
+ mode, types, version, branches,
251
243
  counters: { workflows: result?.workflows?.copied ?? 0 },
244
+ gitignoreUpdated: result?.gitignoreUpdated === true,
252
245
  }, cwd);
253
246
  io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
254
247
  return 0;
@@ -1,10 +1,10 @@
1
- // purge 모드 — revert가 지우는 전부(워크플로우·스크립트·coderabbit) + version.yml·README
1
+ // purge 모드 — revert가 지우는 전부(워크플로우·스크립트) + version.yml·README
2
2
  // AUTO-VERSION-SECTION 블록·CHANGELOG를 추가로 제거해 설치 이전 상태로 완전히 되돌린다.
3
3
  // 개발·테스트 전용 숨김 모드 — DESIGN-SPEC purge #6.
4
4
  // develop 브랜치 삭제는 파일 삭제와 성격이 달라(실행 시점 git 상태 판단 필요) 여기 plan에는
5
5
  // 포함하지 않고 index.js의 purge 분기에서 직접 처리한다.
6
6
  import { join } from "node:path";
7
- import { existsSync, renameSync } from "node:fs";
7
+ import { existsSync } from "node:fs";
8
8
  import { PATHS } from "../core/paths.js";
9
9
  import { remove } from "../core/fsutil.js";
10
10
  import { planRevert } from "./revert.js";
@@ -12,14 +12,13 @@ import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/
12
12
 
13
13
  const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
14
14
 
15
- // keepFlags: { versionYml, readme, changelog, workflows, scripts, coderabbit } — true인 카테고리는 후보에서 제외.
16
- // 반환: { workflows, scripts, coderabbit, versionYml, readmeSection, changelog } — 아무것도 지우지 않는 순수 함수.
15
+ // keepFlags: { versionYml, readme, changelog, workflows, scripts } — true인 카테고리는 후보에서 제외.
16
+ // 반환: { workflows, scripts, versionYml, readmeSection, changelog } — 아무것도 지우지 않는 순수 함수.
17
17
  export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
18
18
  const revertPlan = planRevert(payloadRoot, targetRoot);
19
19
  return {
20
20
  workflows: keepFlags.workflows ? [] : revertPlan.workflows,
21
21
  scripts: keepFlags.scripts ? [] : revertPlan.scripts,
22
- coderabbit: keepFlags.coderabbit ? false : revertPlan.coderabbit,
23
22
  versionYml: !keepFlags.versionYml && existsSync(join(targetRoot, PATHS.versionFile)),
24
23
  readmeSection: !keepFlags.readme && hasVersionSection(targetRoot),
25
24
  changelog: keepFlags.changelog ? [] : CHANGELOG_FILES.filter((f) => existsSync(join(targetRoot, f))),
@@ -37,7 +36,6 @@ export function printPurgePlan(plan, { dryRun = false } = {}) {
37
36
  for (const f of plan.workflows) lines.push(` - ${f}`);
38
37
  lines.push(`스크립트 (${plan.scripts.length}개):`);
39
38
  for (const f of plan.scripts) lines.push(` - ${f}`);
40
- if (plan.coderabbit) lines.push("파일: .coderabbit.yaml");
41
39
  if (plan.versionYml) lines.push("파일: version.yml");
42
40
  if (plan.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
43
41
  for (const f of plan.changelog) lines.push(`파일: ${f}`);
@@ -57,11 +55,6 @@ export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
57
55
  const wfDir = join(targetRoot, PATHS.workflowsDir);
58
56
  for (const name of plan.workflows) remove(join(wfDir, name));
59
57
  for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
60
- if (plan.coderabbit) {
61
- const cr = join(targetRoot, ".coderabbit.yaml");
62
- remove(cr);
63
- if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
64
- }
65
58
  if (plan.versionYml) remove(join(targetRoot, PATHS.versionFile));
66
59
  const readmeSection = plan.readmeSection && removeVersionSectionFromReadme(targetRoot) === "removed";
67
60
  for (const f of plan.changelog) remove(join(targetRoot, f));
@@ -76,11 +69,10 @@ export function printPurgeResult(result) {
76
69
  for (const f of result.workflows) lines.push(` - ${f}`);
77
70
  lines.push(`스크립트 (${result.scripts.length}개):`);
78
71
  for (const f of result.scripts) lines.push(` - ${f}`);
79
- if (result.coderabbit) lines.push("파일: .coderabbit.yaml");
80
72
  if (result.versionYml) lines.push("파일: version.yml");
81
73
  if (result.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
82
74
  for (const f of result.changelog) lines.push(`파일: ${f}`);
83
- lines.push("(.gitignore 자동 추가 블록은 보존됩니다 별도 이슈에서 다룸)");
75
+ lines.push("(.gitignore 추가된 백업 파일 제외 항목(*.bak/*.template.yaml)은 purge 대상에서 제외되어 그대로 보존됩니다)");
84
76
  lines.push("");
85
77
  console.log(lines.join("\n"));
86
78
  }
@@ -3,7 +3,7 @@
3
3
  // 사용자가 직접 만든 워크플로우·version.yml·README·.gitignore는 건드리지 않는다
4
4
  // (version.yml은 사용자 버전 데이터 — 제거 대상이 아니라 산출물이다).
5
5
  import { join } from "node:path";
6
- import { existsSync, readdirSync, readFileSync, renameSync } from "node:fs";
6
+ import { existsSync, readdirSync } from "node:fs";
7
7
  import { PATHS, PAYLOAD } from "../core/paths.js";
8
8
  import { remove } from "../core/fsutil.js";
9
9
 
@@ -37,25 +37,14 @@ export function planRevert(payloadRoot, targetRoot = ".") {
37
37
  for (const s of ["version_manager.py", "changelog_manager.py"]) {
38
38
  if (existsSync(join(targetRoot, PATHS.scriptsDir, s))) removedScripts.push(s);
39
39
  }
40
- let coderabbit = false;
41
- const cr = join(targetRoot, ".coderabbit.yaml");
42
- const crSrc = join(payloadRoot, "coderabbit.yaml");
43
- if (existsSync(cr) && existsSync(crSrc) && readFileSync(cr, "utf8") === readFileSync(crSrc, "utf8")) {
44
- coderabbit = true;
45
- }
46
- return { workflows: removedWf, scripts: removedScripts, coderabbit };
40
+ return { workflows: removedWf, scripts: removedScripts };
47
41
  }
48
42
 
49
- // 반환: { workflows: [...제거된 파일명], scripts: [...], coderabbit: bool } — planRevert와 동일한 형태.
43
+ // 반환: { workflows: [...제거된 파일명], scripts: [...] } — planRevert와 동일한 형태.
50
44
  export function runRevert(context, payloadRoot, targetRoot = ".") {
51
45
  const plan = planRevert(payloadRoot, targetRoot);
52
46
  const wfDir = join(targetRoot, PATHS.workflowsDir);
53
47
  for (const name of plan.workflows) remove(join(wfDir, name));
54
48
  for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
55
- if (plan.coderabbit) {
56
- const cr = join(targetRoot, ".coderabbit.yaml");
57
- remove(cr);
58
- if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
59
- }
60
49
  return plan;
61
50
  }
@@ -52,7 +52,7 @@ export function printStatus(status) {
52
52
  }
53
53
  const boolLabel = (v) => (v === null ? "미설정(기본 false)" : v);
54
54
  const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
55
- lines.push(`옵션 : nexus=${boolLabel(status.options.nexus)} secret_backup=${boolLabel(status.options.secretBackup)} coderabbit=${boolLabel(status.options.coderabbit)} semver_auto=${semverAutoLabel}`);
55
+ lines.push(`옵션 : nexus=${boolLabel(status.options.nexus)} secret_backup=${boolLabel(status.options.secretBackup)} semver_auto=${semverAutoLabel}`);
56
56
  if (status.modifiedFiles.length) {
57
57
  lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
58
58
  for (const f of status.modifiedFiles) lines.push(` - ${f}`);
@@ -1,7 +1,7 @@
1
1
  // uninstall 모드 — revert(payload 파일명 일치분)보다 넓게, README/.gitignore/version.yml까지 선택적으로 제거.
2
2
  // 대화형 체크리스트 또는 --force + --purge-* 로 항목별 opt-in. revert.js는 건드리지 않고 읽기 전용으로만 재사용한다.
3
3
  import { join } from "node:path";
4
- import { existsSync, renameSync } from "node:fs";
4
+ import { existsSync } from "node:fs";
5
5
  import { PATHS } from "../core/paths.js";
6
6
  import { remove } from "../core/fsutil.js";
7
7
  import { planRevert } from "./revert.js";
@@ -9,14 +9,13 @@ import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/
9
9
  import { removeAutoAddedEntriesFromGitignore, hasAutoAddedEntries } from "../core/copy/gitignore.js";
10
10
  import { CANCEL } from "../ui/prompts.js";
11
11
 
12
- // selection: { workflows, scripts, coderabbit, readme, gitignore, versionYml } (모두 boolean).
12
+ // selection: { workflows, scripts, readme, gitignore, versionYml } (모두 boolean).
13
13
  // 반환: 위와 동일한 키의 boolean/배열 — 실제로 제거 "대상"인지 여부(순수 함수, 아무것도 지우지 않음).
14
14
  export function planUninstall(payloadRoot, targetRoot, selection) {
15
15
  const revertPlan = planRevert(payloadRoot, targetRoot);
16
16
  return {
17
17
  workflows: selection.workflows ? revertPlan.workflows : [],
18
18
  scripts: selection.scripts ? revertPlan.scripts : [],
19
- coderabbit: selection.coderabbit ? revertPlan.coderabbit : false,
20
19
  readme: selection.readme ? hasVersionSection(targetRoot) : false,
21
20
  gitignore: selection.gitignore ? hasAutoAddedEntries(targetRoot) : false,
22
21
  versionYml: selection.versionYml ? existsSync(join(targetRoot, PATHS.versionFile)) : false,
@@ -29,11 +28,6 @@ export function runUninstall(context, payloadRoot, targetRoot, selection) {
29
28
  const wfDir = join(targetRoot, PATHS.workflowsDir);
30
29
  for (const name of plan.workflows) remove(join(wfDir, name));
31
30
  for (const name of plan.scripts) remove(join(targetRoot, PATHS.scriptsDir, name));
32
- if (plan.coderabbit) {
33
- const cr = join(targetRoot, ".coderabbit.yaml");
34
- remove(cr);
35
- if (existsSync(cr + ".bak")) renameSync(cr + ".bak", cr);
36
- }
37
31
  // removeVersionSectionFromReadme/removeAutoAddedEntriesFromGitignore는 plan이 "제거 대상"으로
38
32
  // 판단했더라도 실제로는 안전하게 포기(skip-*)할 수 있다 — 반환 상태를 그대로 신뢰하지 않고
39
33
  // 실제 결과로 plan을 덮어써서 호출부(CLI/대화형 요약)가 거짓 성공을 보고하지 않게 한다.
@@ -48,22 +42,20 @@ export function runUninstall(context, payloadRoot, targetRoot, selection) {
48
42
  const ITEM_DEFS = [
49
43
  { key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml)" },
50
44
  { key: "scripts", label: "스크립트 (.github/scripts/*.py)" },
51
- { key: "coderabbit", label: ".coderabbit.yaml" },
52
45
  { key: "readme", label: "README.md 버전 섹션 (AUTO-VERSION-SECTION)" },
53
46
  { key: "gitignore", label: ".gitignore 자동 추가 항목" },
54
47
  { key: "versionYml", label: "version.yml (버전/브랜치 설정 전체)" },
55
48
  ];
56
49
 
57
- // 기본 체크 상태 — 설치 시 옵션(nexus/secret-backup/coderabbit)이 opt-in인 것과 대칭으로,
58
- // 여기서는 "안전 삭제" 3종만 기본 체크하고 나머지(readme/gitignore/versionYml)는 opt-in.
59
- export const SAFE_ITEMS = ["workflows", "scripts", "coderabbit"];
50
+ // 기본 체크 상태 — 설치 시 옵션(nexus/secret-backup)이 opt-in인 것과 대칭으로,
51
+ // 여기서는 "안전 삭제" 2종만 기본 체크하고 나머지(readme/gitignore/versionYml)는 opt-in.
52
+ export const SAFE_ITEMS = ["workflows", "scripts"];
60
53
 
61
54
  function detectAvailableItems(payloadRoot, targetRoot) {
62
55
  const revertPlan = planRevert(payloadRoot, targetRoot);
63
56
  const presence = {
64
57
  workflows: revertPlan.workflows.length > 0,
65
58
  scripts: revertPlan.scripts.length > 0,
66
- coderabbit: revertPlan.coderabbit,
67
59
  readme: hasVersionSection(targetRoot),
68
60
  gitignore: hasAutoAddedEntries(targetRoot),
69
61
  versionYml: existsSync(join(targetRoot, PATHS.versionFile)),
@@ -74,7 +66,7 @@ function detectAvailableItems(payloadRoot, targetRoot) {
74
66
  function toSelection(checkedKeys) {
75
67
  const set = new Set(checkedKeys);
76
68
  return {
77
- workflows: set.has("workflows"), scripts: set.has("scripts"), coderabbit: set.has("coderabbit"),
69
+ workflows: set.has("workflows"), scripts: set.has("scripts"),
78
70
  readme: set.has("readme"), gitignore: set.has("gitignore"), versionYml: set.has("versionYml"),
79
71
  };
80
72
  }
@@ -89,7 +81,6 @@ function summarizeResult(result) {
89
81
  const lines = [];
90
82
  if (result.workflows.length) lines.push(`워크플로우 ${result.workflows.length}개 제거`);
91
83
  if (result.scripts.length) lines.push(`스크립트 ${result.scripts.length}개 제거`);
92
- if (result.coderabbit) lines.push(".coderabbit.yaml 제거");
93
84
  if (result.readme) lines.push("README.md 버전 섹션 제거");
94
85
  if (result.gitignore) lines.push(".gitignore 자동 추가 항목 제거");
95
86
  if (result.versionYml) lines.push("version.yml 제거");
@@ -1,6 +1,7 @@
1
1
  // version 모드 (.sh execute_integration version case 등가).
2
- // 순서: version.yml → readme → scripts → gitignore.
3
- // (워크플로우 미복사 deploy 블록 없음. util·issue·coderabbit·setup-guide는 스코프 제외.)
2
+ // 순서: version.yml → readme → scripts.
3
+ // (워크플로우를 복사하지 않으므로 충돌 백업 부산물이 생길 수 없다 — gitignore 갱신 대상 없음, issue #7.
4
+ // util·issue·setup-guide는 스코프 제외.)
4
5
  import { join } from "node:path";
5
6
  import { writeText } from "../core/fsutil.js";
6
7
  import { PATHS } from "../core/paths.js";
@@ -9,12 +10,11 @@ import { readVersionYmlTemplate } from "../core/assets.js";
9
10
  import { markerForType } from "../core/detect.js";
10
11
  import { addVersionSectionToReadme } from "../core/copy/readme.js";
11
12
  import { copyScripts } from "../core/copy/simple.js";
12
- import { ensureGitignore } from "../core/copy/gitignore.js";
13
13
 
14
14
  export function runVersion(context, payloadRoot, targetRoot = ".") {
15
15
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
16
16
  now, today, templateVersion = "unknown",
17
- includeNexus = false, includeSecretBackup = false, includeCodeRabbit = false,
17
+ includeNexus = false, includeSecretBackup = false,
18
18
  includeSemverAuto } = context;
19
19
 
20
20
  const pathMarkers = new Map();
@@ -24,9 +24,8 @@ export function runVersion(context, payloadRoot, targetRoot = ".") {
24
24
  buildVersionYml({
25
25
  templateText: readVersionYmlTemplate(payloadRoot),
26
26
  version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
27
- templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeCodeRabbit: includeCodeRabbit === true, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
27
+ templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
28
28
  }));
29
29
  addVersionSectionToReadme(version, targetRoot);
30
30
  copyScripts(payloadRoot, targetRoot);
31
- ensureGitignore(targetRoot);
32
31
  }
package/src/context.js CHANGED
@@ -17,7 +17,6 @@ export function createContext(overrides = {}) {
17
17
  paths: new Map(), // type -> path
18
18
  includeNexus: null, // null=미설정, true/false=명시
19
19
  includeSecretBackup: null,
20
- includeCodeRabbit: null, // CodeRabbit opt-in (기본 false — version.yml options.coderabbit에 기록)
21
20
  includeSemverAuto: null, // null=미설정(다운스트림에서 true로 해석), true/false=명시
22
21
  templateVersion: "",
23
22
  deployValues: new Map(), // "type.KEY" -> value
@@ -1,8 +1,17 @@
1
- // .gitignore 보장 (.sh ensure_gitignore + normalize/check 등가) template_integrator.sh 3996~4111.
1
+ // .gitignore 보장 마법사 자신이 만드는 충돌 백업 부산물(*.bak, *.template.yaml)만 대상으로 한다.
2
+ // 마법사가 설치하는 것과 무관한 개인 개발환경 설정(IDE 등)은 마법사 책임 범위 밖 — issue #7.
3
+ // 주의(의도된 트레이드오프): 이 변경 이전 버전으로 설치해 /.idea·/.claude/settings.local.json이 이미
4
+ // 배너 블록에 남아있는 레포에서 removeAutoAddedEntriesFromGitignore()를 실행하면, 배너는 제거되지만
5
+ // REQUIRED_ENTRIES가 더 이상 옛 항목과 일치하지 않아 그 두 줄은 지워지지 않고 배너 표시 없이 남는다.
6
+ // 이슈 #7이 "이미 설치된 레포의 .gitignore는 소급 처리하지 않고 사용자 판단에 맡긴다"고 명시하므로
7
+ // 별도 마이그레이션 로직을 추가하지 않는다 — 남은 항목은 무해하며 필요하면 사용자가 직접 지운다.
2
8
  import { join } from "node:path";
3
9
  import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
4
10
 
5
- const REQUIRED_ENTRIES = ["/.idea", "/.claude/settings.local.json"];
11
+ // issue #7: 마법사가 설치하는 것과 무관한 개인 개발환경 항목(/.idea 등)은 마법사 책임 밖이므로 제거.
12
+ // 대신 마법사 자신의 충돌 처리(workflows.js backup/template 결정)가
13
+ // 실제로 만들어내는 부산물만 gitignore 대상으로 삼는다.
14
+ const REQUIRED_ENTRIES = ["*.bak", "*.template.yaml"];
6
15
 
7
16
  // .sh normalize_gitignore_entry: 주석 제거·트림·앞 / 제거·앞 ./ 제거·뒤 / 제거. 빈값이면 원본.
8
17
  export function normalizeGitignoreEntry(entry) {
@@ -26,11 +35,9 @@ function entryExists(target, content) {
26
35
  }
27
36
 
28
37
  const NEW_FILE_CONTENT =
29
- "# IDE Settings\n" +
30
- "/.idea\n" +
31
- "\n" +
32
- "# Claude AI Settings\n" +
33
- "/.claude/settings.local.json\n";
38
+ "# project-auto-wizard: 충돌 처리 시 생성되는 백업 파일 (안전하게 무시해도 됩니다)\n" +
39
+ "*.bak\n" +
40
+ "*.template.yaml\n";
34
41
 
35
42
  // 반환: {created, added:[...]}
36
43
  export function ensureGitignore(targetRoot = ".") {
@@ -58,7 +58,7 @@ function classify(srcDir, workflowsDir, envOpts, srcText) {
58
58
  // envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
59
59
  // hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'> } — 기존 파일(changed) 충돌 결정.
60
60
  // 미지정 파일은 'skip'(현행 force 동작 100% 유지). 대화형 수집은 copyWorkflowsInteractive 참조.
61
- // 반환: {copied, skipped, templateAdded, optionalCopied}
61
+ // 반환: {copied, skipped, templateAdded, optionalCopied, backupAdded}
62
62
  export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}) {
63
63
  const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true } = context;
64
64
  const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
@@ -66,7 +66,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
66
66
  const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
67
67
  if (!exists(projectTypesDir)) throw new Error("패키지 구조 오류 — payload/workflows 폴더를 찾지 못했습니다.");
68
68
 
69
- const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0 };
69
+ const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0, backupAdded: 0 };
70
70
  const deployValues = new Map(); // Map<type, Map<key,value>> — deploy 블록용 ask 값
71
71
  counters.deployValues = deployValues;
72
72
  const srcText = makeSrcText(context.branches || null);
@@ -124,6 +124,7 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
124
124
  renameSync(dst, dst + ".bak");
125
125
  writeText(dst, srcText(src));
126
126
  counters.copied++;
127
+ counters.backupAdded++;
127
128
  return;
128
129
  }
129
130
  if (decision === "template") {
@@ -213,7 +214,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
213
214
  counters.skipped++;
214
215
  continue;
215
216
  }
216
- if (existsSync(dst)) renameSync(dst, dst + ".bak");
217
+ if (existsSync(dst)) { renameSync(dst, dst + ".bak"); counters.backupAdded++; }
217
218
  writeText(dst, body);
218
219
  counters.optionalCopied++;
219
220
  counters.copied++;
@@ -4,10 +4,10 @@
4
4
 
5
5
  // metadata.template.options 상태머신 파싱 (.sh read_template_options L2361~2416 등가).
6
6
  // 반환: { nexus: bool|null, secretBackup: bool|null } — null=미기재.
7
- // 구 synology 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다.
7
+ // 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
8
8
  // (options-ask.js가 이 함수를 import한다 — 순환 방지 위해 여기(version-yml)에 정의.)
9
9
  export function parseTemplateOptions(content) {
10
- const out = { nexus: null, secretBackup: null, coderabbit: null, semverAuto: null };
10
+ const out = { nexus: null, secretBackup: null, semverAuto: null };
11
11
  // 값 정규화: 따옴표 제거 + 트림 (.sh tr -d '"' | tr -d "'" | xargs 등가)
12
12
  const strip = (s) => String(s).replace(/["']/g, "").trim();
13
13
  let inTemplate = false;
@@ -30,13 +30,6 @@ export function parseTemplateOptions(content) {
30
30
  if (v === "false") out.secretBackup = false;
31
31
  continue;
32
32
  }
33
- m = line.match(/^\s+coderabbit:\s*(.+)/);
34
- if (m) {
35
- const v = strip(m[1]);
36
- if (v === "true") out.coderabbit = true;
37
- if (v === "false") out.coderabbit = false;
38
- continue;
39
- }
40
33
  m = line.match(/^\s+semver_auto:\s*(.+)/);
41
34
  if (m) {
42
35
  const v = strip(m[1]);
@@ -132,7 +125,7 @@ export function parseTemplateBranches(content) {
132
125
  // now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입 / today = "YYYY-MM-DD"
133
126
  // branches = { main, develop, mode } (resolveBranchConfig 결과. 없으면 branch 기반 기본값)
134
127
  // pathMarkers = Map<type, markerFilename> (project_paths 주석용)
135
- // templateOptions = { templateVersion, includeNexus, includeSecretBackup, includeCodeRabbit?, optionsDate }
128
+ // templateOptions = { templateVersion, includeNexus, includeSecretBackup, optionsDate }
136
129
  export function buildVersionYml({
137
130
  templateText, version, types = [], primaryType, paths = new Map(), pathMarkers = new Map(),
138
131
  branch = "main", branches = null, versionCode = 1, now, today,
@@ -144,7 +137,7 @@ export function buildVersionYml({
144
137
  const b = branches || { main: branch || "main", develop: "develop", mode: "pr-flow" };
145
138
  const {
146
139
  templateVersion = "unknown", includeNexus = false, includeSecretBackup = false,
147
- includeCodeRabbit = false, includeSemverAuto = true, optionsDate = today,
140
+ includeSemverAuto = true, optionsDate = today,
148
141
  } = templateOptions || {};
149
142
 
150
143
  // project_paths 블록 (full-line 토큰 {{PROJECT_PATHS}} — 없으면 라인 제거)
@@ -178,7 +171,7 @@ export function buildVersionYml({
178
171
  TEMPLATE_VERSION: templateVersion,
179
172
  MAIN_BRANCH: b.main, DEVELOP_BRANCH: b.develop, BRANCH_MODE: b.mode,
180
173
  OPT_NEXUS: String(includeNexus), OPT_SECRET_BACKUP: String(includeSecretBackup),
181
- OPT_CODERABBIT: String(includeCodeRabbit), OPT_SEMVER_AUTO: String(includeSemverAuto),
174
+ OPT_SEMVER_AUTO: String(includeSemverAuto),
182
175
  };
183
176
 
184
177
  const out = [];
package/src/index.js CHANGED
@@ -103,7 +103,7 @@ export async function run(argv, {
103
103
  return 0;
104
104
  }
105
105
  const r = runRevert({}, payload, cwd);
106
- console.error(`제거됨 — 워크플로우 ${r.workflows.length}개, 스크립트 ${r.scripts.length}개${r.coderabbit ? ", .coderabbit.yaml" : ""}`);
106
+ console.error(`제거됨 — 워크플로우 ${r.workflows.length}개, 스크립트 ${r.scripts.length}개`);
107
107
  console.error("version.yml·README·.gitignore는 보존됩니다 (사용자 데이터).");
108
108
  return 0;
109
109
  }
@@ -116,7 +116,7 @@ export async function run(argv, {
116
116
  }
117
117
  const keepFlags = {
118
118
  versionYml: opts.keepVersionYml, readme: opts.keepReadme, changelog: opts.keepChangelog,
119
- workflows: opts.keepWorkflows, scripts: opts.keepScripts, coderabbit: opts.keepCoderabbit,
119
+ workflows: opts.keepWorkflows, scripts: opts.keepScripts,
120
120
  };
121
121
  // version.yml은 여기서 미리 읽어둔다 — (a) executePurge()가 version.yml 자체를 지울 수 있어
122
122
  // 실행 이후에는 읽을 수 없고, (b) 아래 dry-run 예고 문구도 trunk-based 여부(develop === main)를
@@ -185,7 +185,7 @@ export async function run(argv, {
185
185
  // uninstall 모드 — revert보다 넓게 README·gitignore·version.yml까지 선택적으로 제거.
186
186
  if (opts.mode === "uninstall") {
187
187
  const safeSelection = {
188
- workflows: true, scripts: true, coderabbit: true,
188
+ workflows: true, scripts: true,
189
189
  readme: opts.purgeReadme, gitignore: opts.purgeGitignore, versionYml: opts.purgeVersion,
190
190
  };
191
191
  if (opts.dryRun) {
@@ -196,7 +196,7 @@ export async function run(argv, {
196
196
  const r = runUninstall({}, payload, cwd, safeSelection);
197
197
  const removed = [
198
198
  `워크플로우 ${r.workflows.length}개`, `스크립트 ${r.scripts.length}개`,
199
- r.coderabbit && ".coderabbit.yaml", r.readme && "README 버전 섹션",
199
+ r.readme && "README 버전 섹션",
200
200
  r.gitignore && ".gitignore 자동 추가 항목", r.versionYml && "version.yml",
201
201
  ].filter(Boolean).join(", ");
202
202
  console.error(`제거됨 — ${removed}`);
@@ -271,7 +271,6 @@ export async function run(argv, {
271
271
  // 옵션 워크플로우: CLI 플래그 최우선 → version.yml 저장 옵션(.sh read_template_options 등가) → false
272
272
  includeNexus: opts.includeNexus ?? existing?.options?.nexus ?? false,
273
273
  includeSecretBackup: opts.includeSecretBackup ?? existing?.options?.secretBackup ?? false,
274
- includeCodeRabbit: opts.includeCodeRabbit ?? existing?.options?.coderabbit ?? false,
275
274
  // 기존 version.yml이 있는데 semver_auto 키가 아예 없었던 경우(신규 기능 추가 이전 설치·
276
275
  // workflows-only 재실행) 조용히 true로 켜지면 애매한 커밋 하나로 major가 승격될 위험이 있다 —
277
276
  // 기존 설치는 false로 안전하게 폴백, 완전 신규 설치만 true(기존 설계) 유지.
@@ -309,8 +308,8 @@ export async function run(argv, {
309
308
  // 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
310
309
  printSummary({
311
310
  mode: opts.mode, types, version, branches,
312
- includeCodeRabbit: context.includeCodeRabbit === true,
313
311
  counters: { workflows: result?.workflows?.copied ?? 0 },
312
+ gitignoreUpdated: result?.gitignoreUpdated === true,
314
313
  }, cwd);
315
314
  return 0;
316
315
  }
package/src/ui/summary.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // 완료 요약 출력 (.sh print_summary 등가). 전부 stderr.
2
- // ctx: { mode, types:[], version, counters:{ workflows }, branches?, includeCodeRabbit? }
2
+ // ctx: { mode, types:[], version, counters:{ workflows }, branches? }
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { PATHS, WORKFLOW_PREFIX, WORKFLOW_COMMON_PREFIX } from "../core/paths.js";
@@ -8,7 +8,7 @@ import { listYamlFiles } from "../core/fsutil.js";
8
8
  const SEPARATOR = "────────────────────────────────────────";
9
9
 
10
10
  export function printSummary(ctx, targetRoot = ".") {
11
- const { mode, types = [], version = "", counters = {}, branches = null, includeCodeRabbit = false } = ctx || {};
11
+ const { mode, types = [], version = "", counters = {}, branches = null, gitignoreUpdated = false } = ctx || {};
12
12
  const err = (s = "") => process.stderr.write(`${s}\n`);
13
13
  // 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
14
14
  const isTty = !!process.stderr.isTTY;
@@ -32,12 +32,11 @@ export function printSummary(ctx, targetRoot = ".") {
32
32
  err(" ✅ 버전 관리 시스템 (version.yml)");
33
33
  err(" ✅ README.md 자동 버전 업데이트");
34
34
  err(" ✅ GitHub Actions 워크플로우 (AI 릴리스 자동화 포함)");
35
- err(" ✅ .gitignore 필수 항목");
35
+ if (gitignoreUpdated) err(" ✅ .gitignore 백업 파일 제외 항목 (*.bak/*.template.yaml)");
36
36
  break;
37
37
  case "version":
38
38
  err(" ✅ 버전 관리 시스템 (version.yml)");
39
39
  err(" ✅ README.md 자동 버전 업데이트");
40
- err(" ✅ .gitignore 필수 항목");
41
40
  break;
42
41
  case "workflows":
43
42
  err(" ✅ GitHub Actions 워크플로우 (AI 릴리스 자동화 포함)");
@@ -57,8 +56,7 @@ export function printSummary(ctx, targetRoot = ".") {
57
56
  if (mode === "full" || mode === "workflows") {
58
57
  err("");
59
58
  err("릴리스 노트 요약 엔진:");
60
- if (includeCodeRabbit) err(" 🤖 1순위 CodeRabbit PR 요약 (opt-in) → AI_API_KEY → GitHub Models(무료) → 규칙 fallback");
61
- else err(" 🤖 AI_API_KEY(선택) → GitHub Models(기본·무료·API 키 불필요) → 규칙 fallback — 릴리스는 절대 막히지 않음");
59
+ err(" 🤖 AI_API_KEY(선택) → GitHub Models(기본·무료·API 키 불필요) → 규칙 fallback — 릴리스는 절대 막히지 않음");
62
60
  }
63
61
 
64
62
  err("");
@@ -1,19 +0,0 @@
1
- # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2
- language: "ko-KR"
3
- early_access: false
4
- reviews:
5
- profile: "chill"
6
- request_changes_workflow: false
7
- high_level_summary: true
8
- poem: false
9
- review_status: true
10
- collapse_walkthrough: false
11
- auto_review:
12
- enabled: true
13
- drafts: false
14
- base_branches: ["main"]
15
- chat:
16
- auto_reply: true
17
- issue_enrichment:
18
- auto_enrich:
19
- enabled: false
@@ -1,26 +0,0 @@
1
- // .coderabbit.yaml 복사 (.sh copy_coderabbit_config 등가, force 경로).
2
- // 소스는 payload/coderabbit.yaml (npm 패키징 안전을 위해 무점 파일명으로 동봉 → 대상엔 .coderabbit.yaml).
3
- // Task 15에서 coderabbit opt-in true일 때만 호출하도록 재배선 예정.
4
- import { join } from "node:path";
5
- import { exists, copyFileSync } from "../fsutil.js";
6
-
7
- // 반환: 'skip-no-src' | 'copied-new' | 'overwritten-backup' | 'skip-non-tty'
8
- // opts: { force, tty }
9
- export function copyCoderabbit(payloadRoot, { force = false, tty = false } = {}, targetRoot = ".") {
10
- const src = join(payloadRoot, "coderabbit.yaml");
11
- if (!exists(src)) return "skip-no-src";
12
- const dst = join(targetRoot, ".coderabbit.yaml");
13
-
14
- if (exists(dst)) {
15
- if (force) {
16
- copyFileSync(dst, dst + ".bak"); // 백업 후 덮어쓰기
17
- copyFileSync(src, dst);
18
- return "overwritten-backup";
19
- }
20
- if (!tty) return "skip-non-tty"; // 비TTY & !force → 유지
21
- // 대화형 메뉴는 SP2-C — 여기서는 force가 아니면 유지
22
- return "skip-non-tty";
23
- }
24
- copyFileSync(src, dst); // 신규
25
- return "copied-new";
26
- }