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 +9 -11
- package/package.json +1 -1
- package/payload/scripts/changelog_manager.py +55 -43
- package/payload/version.yml.template +0 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +1 -13
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +5 -101
- package/src/cli/args.js +0 -5
- package/src/cli/help.js +0 -1
- package/src/commands/dry-run.js +1 -4
- package/src/commands/full.js +9 -8
- package/src/commands/interactive.js +4 -11
- package/src/commands/purge.js +5 -13
- package/src/commands/revert.js +3 -14
- package/src/commands/status.js +1 -1
- package/src/commands/uninstall.js +6 -15
- package/src/commands/version.js +5 -6
- package/src/context.js +0 -1
- package/src/core/copy/gitignore.js +14 -7
- package/src/core/copy/workflows.js +4 -3
- package/src/core/version-yml.js +5 -12
- package/src/index.js +5 -6
- package/src/ui/summary.js +4 -6
- package/payload/coderabbit.yaml +0 -19
- package/src/core/copy/coderabbit.js +0 -26
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx project-auto-wizard
|
|
|
16
16
|
[](package.json)
|
|
17
17
|
|
|
18
18
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
19
|
-
## 최신 버전 : v0.1.
|
|
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`보다 넓게 제거합니다 —
|
|
78
|
+
`npx project-auto-wizard --mode uninstall`은 `revert`보다 넓게 제거합니다 — 워크플로우·스크립트는 물론, README.md의 `AUTO-VERSION-SECTION` 버전 섹션과 `.gitignore`에 자동 추가된 항목, `version.yml`까지 선택적으로 제거할 수 있습니다.
|
|
79
79
|
|
|
80
|
-
- **대화형(TTY)**: 실제로 설치된 항목만 체크리스트로 보여줍니다.
|
|
81
|
-
- **비대화형(`--force`)**:
|
|
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 #
|
|
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
|
-
릴리스 노트는
|
|
92
|
+
릴리스 노트는 3단 엔진 체인으로 생성됩니다. **상용 서드파티 서비스에 전혀 의존하지 않으며, 어떤 단계가 실패해도 릴리스는 절대 막히지 않습니다.**
|
|
93
93
|
|
|
94
94
|
```mermaid
|
|
95
95
|
flowchart LR
|
|
96
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
@@ -5,7 +5,7 @@ changelog_manager.py
|
|
|
5
5
|
통합 체인지로그 매니저 스크립트.
|
|
6
6
|
|
|
7
7
|
서브커맨드:
|
|
8
|
-
- update-from-summary:
|
|
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.
|
|
50
|
-
3.
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
77
|
+
릴리즈 요약 Markdown을 카테고리/항목으로 파싱.
|
|
84
78
|
|
|
85
79
|
3단계 폴백 전략:
|
|
86
|
-
1.
|
|
87
|
-
2. 관대한 파싱 (형식 변형 대응)
|
|
80
|
+
1. 섹션 파싱 (AI 엔진 체인·규칙 기반 폴백이 실제로 생성하는 형식)
|
|
81
|
+
2. 관대한 파싱 (중첩 불릿 등 형식 변형 대응)
|
|
88
82
|
3. 휴리스틱 파싱 (최후 수단)
|
|
89
83
|
|
|
90
|
-
예상
|
|
91
|
-
##
|
|
84
|
+
예상 형식 (_build_ai_prompt / render_fallback_md가 지정하는 형식):
|
|
85
|
+
## [1.2.3]
|
|
92
86
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
### ✨ 기능
|
|
88
|
+
- 사용자 로그인 추가
|
|
89
|
+
- 대시보드 위젯 추가
|
|
96
90
|
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
### 🐛 수정
|
|
92
|
+
- 널 포인터 예외 수정
|
|
99
93
|
"""
|
|
100
|
-
# 1단계:
|
|
101
|
-
detected =
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
섹션 파서: `### 카테고리` 헤딩 + `- 항목` 불릿 형식.
|
|
122
123
|
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
146
|
+
if current_key is None:
|
|
139
147
|
continue
|
|
140
148
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ===================================================================
|
|
2
2
|
# PROJECT-COMMON-AI-PR-SUMMARY.yaml
|
|
3
|
-
# Self-hosted AI PR summary bot —
|
|
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.
|
|
14
|
-
# -
|
|
15
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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 전체 지원)
|
package/src/commands/dry-run.js
CHANGED
|
@@ -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,
|
|
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");
|
package/src/commands/full.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// full 모드 오케스트레이터 (.sh execute_integration full case 등가).
|
|
2
|
-
// 복사 순서: workflows(+env 치환) → version.yml → readme → scripts →
|
|
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,
|
|
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,
|
|
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.
|
|
50
|
-
|
|
51
|
-
|
|
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}개
|
|
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,
|
|
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,
|
|
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;
|
package/src/commands/purge.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// purge 모드 — revert가 지우는 전부(
|
|
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
|
|
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
|
|
16
|
-
// 반환: { workflows, scripts,
|
|
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
|
}
|
package/src/commands/revert.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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: [...]
|
|
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
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -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)}
|
|
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
|
|
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,
|
|
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
|
|
58
|
-
// 여기서는 "안전 삭제"
|
|
59
|
-
export const SAFE_ITEMS = ["workflows", "scripts"
|
|
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"),
|
|
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 제거");
|
package/src/commands/version.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// version 모드 (.sh execute_integration version case 등가).
|
|
2
|
-
// 순서: version.yml → readme → scripts
|
|
3
|
-
// (
|
|
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,
|
|
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,
|
|
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 보장
|
|
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
|
-
|
|
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
|
-
"#
|
|
30
|
-
"
|
|
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++;
|
package/src/core/version-yml.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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("");
|
package/payload/coderabbit.yaml
DELETED
|
@@ -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
|
-
}
|