projectops 4.41.0 → 4.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  **Status: actively maintained.** npm `latest` is exactly what is on the `main` branch; work in progress on `develop` is never published. Versions can still ship several times a day. To stay on a version you have tested, run `npx projectops@<version>` ([releases](https://github.com/Cassiiopeia/projectops/releases)).
10
10
 
11
11
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
12
- ## Latest version : v4.40.0 (2026-10-10)
12
+ ## Latest version : v4.42.0 (2026-10-10)
13
13
 
14
14
  [View full version history](CHANGELOG.md)
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectops",
3
- "version": "4.41.0",
3
+ "version": "4.43.0",
4
4
  "description": "ProjectOps: installer CLI for a fully automated GitHub project management template (versioning, CI/CD, release notes, agent skills)",
5
5
  "keywords": [
6
6
  "devops",
package/src/cli/args.js CHANGED
@@ -33,6 +33,7 @@ export function parseArgs(argv) {
33
33
  intent: null, // 프로젝트 성격 (#485): --intent app|library|both|none|manual, null=미설정(역추론)
34
34
  includeSecretBackup: null,
35
35
  aiPrSummary: null, // #566 — AI 변경 요약 워크플로우 포함 여부
36
+ codeReviewCoderabbit: null, // --coderabbit: CodeRabbit 코드 리뷰 설정(.coderabbit.yaml) 설치 여부 (null=미지정 → 저장값, 없으면 꺼짐)
36
37
  projectsSync: null, // #716 — Projects 보드 동기화 워크플로우 포함 여부 (null=미지정)
37
38
  json: false, // --json: 기계가 읽는 출력 (--mode options)
38
39
  removeLegacy: false, // #809 — 구세대 배포 워크플로우(confirm 티어)도 .bak 으로 치운다
@@ -169,6 +170,8 @@ export function parseArgs(argv) {
169
170
  case "--secret-backup": result.includeSecretBackup = true; break;
170
171
  case "--no-secret-backup": result.includeSecretBackup = false; break;
171
172
  // #566 — 비대화형에서도 켜고 끌 수 있어야 한다. 없으면 자동화 환경은 선택권이 없다.
173
+ case "--coderabbit": result.codeReviewCoderabbit = true; break;
174
+ case "--no-coderabbit": result.codeReviewCoderabbit = false; break;
172
175
  case "--ai-summary": result.aiPrSummary = true; break;
173
176
  case "--no-ai-summary": result.aiPrSummary = false; break;
174
177
  case "--projects-sync": result.projectsSync = true; break;
package/src/cli/help.js CHANGED
@@ -1,86 +1,167 @@
1
1
  // --help text in each supported language. English is the default (see src/i18n).
2
+ //
3
+ // 이 화면은 사람보다 AI agent 가 더 많이 읽는다. 그래서 "무엇을 하는가"만이 아니라
4
+ // 언제 질문이 나오는지 · 안 주면 무엇이 되는지 · 레포에서 무엇이 바뀌는지를 적는다.
5
+ // 값 목록과 플래그별 상세는 src/core/options-schema.js 가 정본이고 `--mode options --json` 이 그대로 출력한다.
6
+ // test/cli-docs-sync.test.js 가 args.js 의 모든 플래그가 영·한 두 본문에 있는지 검사한다 — 새 플래그는 둘 다에 적는다.
2
7
  import { getLang } from "../i18n/index.js";
3
8
 
4
- const HELP_EN = `projectops — GitHub project automation template installer
9
+ const HELP_EN = `projectops — installs and updates GitHub automation (versioning, release notes, CI/CD, issue templates, agent skills)
5
10
 
6
11
  Usage:
7
12
  npx projectops [options]
8
13
 
9
- Options:
10
- -m, --mode MODE Mode (full | version | workflows | issues | skills | doctor | options)
11
- doctor: diagnose the installation and repository settings (read-only)
12
- options: print every flag and version.yml key; add --json for agents
13
- default: interactive
14
- -t, --type CSV Project types, comma separated (e.g. spring,react,python)
14
+ An AI agent or CI job should always run: npx projectops --mode <mode> --force [flags]
15
+ and read the full option table first: npx projectops --mode options --json
16
+
17
+ How it decides (read this before scripting)
18
+ - No --mode, or --mode interactive: asks questions in the terminal. Needs a TTY; fails without one.
19
+ - Any other --mode: asks NOTHING. Every value comes from the flags you pass, then the values stored in version.yml,
20
+ then the defaults. Without a TTY, --force is also required.
21
+ - --force: skips every confirmation (including the breaking-changes warning). It never overwrites a file you changed:
22
+ that file is kept and a copy of the new template is saved in .github/.projectops/incoming/.
23
+ - Running the same command twice changes nothing that is already current.
24
+
25
+ Modes (-m, --mode MODE)
26
+ full everything: version.yml, workflows, scripts, issue/PR templates, setup guide (.coderabbit.yaml only with --coderabbit)
27
+ workflows only .github/workflows plus the scripts, config and util they use. version.yml is not rewritten
28
+ version only version.yml, the README version section and the version scripts
29
+ issues only issue and discussion templates
30
+ skills install or update agent skills for supported IDEs
31
+ doctor read-only diagnosis of the install and repository settings. Writes nothing. Exit code 0 even when it finds problems
32
+ options prints every flag and every version.yml key with when-to-use, effect and default (add --json). Writes nothing
33
+ interactive asks questions (default when no --mode is given)
34
+
35
+ Options
36
+ -t, --type CSV project types, comma separated, first = primary (e.g. spring,react,python)
15
37
  supported: spring flutter react react-native
16
38
  react-native-expo node python basic
39
+ omitted: auto-detected from files in the repo root (nothing found = basic)
17
40
  (next was merged into react: use react for Next.js)
18
- --project-version V Initial version of the target project (e.g. 1.0.0). Detected if omitted
19
- --paths "t=p,..." Per-type project paths for monorepos. e.g. flutter=app,react=client
20
- --intent KIND Project kind: app | library | both | none | manual
21
- (inferred from --deploy/--publish if omitted)
22
- --label-style STYLE Status label names: en (status: todo) | ko (legacy 작업전)
23
- --language LANG Language of the issue and PR templates written into your repo: en | ko
24
- --deploy TARGET Deployment, pick one: docker-ssh (default) | vercel | none
25
- --publish CSV Publish targets: nexus,npm,github-packages (default: none)
26
- --deploy-branch NAME Head branch of the release PR (default: develop). Not the default branch
27
- --secret-backup / --no-secret-backup Include / exclude the secret backup workflow
28
- --ai-summary / --no-ai-summary Include / exclude the PR summary workflow
29
- --projects-sync / --no-projects-sync Include / exclude the GitHub Projects status sync workflow (default: off for new installs)
30
- --remove-legacy Rename retired old-generation workflows to .bak (otherwise only listed)
41
+ --project-version V initial version, only when version.yml does not exist yet (an existing value is always kept)
42
+ --paths "t=p,..." per-type project folders for monorepos, e.g. flutter=app,react=client. Folders must exist inside the repo
43
+ --intent KIND app | library | both | none | manual. Decides which deploy/publish questions exist.
44
+ omitted: inferred from --deploy/--publish
45
+ --label-style STYLE en = 'status: todo' labels, ko = legacy Korean names. New installs en, existing installs keep ko
46
+ --language LANG en | ko. Language of issue/PR templates, bot comments and version.yml comments written into the repo
47
+ (not the CLI screen — that is --lang). New installs en, existing installs keep the stored value
48
+ --deploy TARGET where the app is deployed, pick one: docker-ssh (SSH + Docker server workflows) | vercel | none
49
+ omitted: docker-ssh for server stacks; mobile types ignore it
50
+ --publish CSV library registries: nexus,npm,github-packages. omitted: none
51
+ --deploy-branch NAME the DEVELOPMENT branch (head of the release PR), default develop. It is NOT the branch that deploys;
52
+ that is the repository default branch. This flag does not create the branch
53
+ --secret-backup / --no-secret-backup upload GitHub Secrets to your server over SSH (needs SERVER_* secrets). omitted: excluded
54
+ --ai-summary / --no-ai-summary AI change summary comment on work PRs. Works with no extra setup. omitted: included
55
+ --coderabbit / --no-coderabbit install .coderabbit.yaml (only --mode full). Needs the CodeRabbit GitHub app. An existing
56
+ file is overwritten and saved as .bak. omitted: off, unless version.yml already stores on.
57
+ This is PR code review only; release notes do not depend on it
58
+ --projects-sync / --no-projects-sync keep a GitHub Projects board's Status in step with issue labels. Needs the secret
59
+ _GITHUB_PAT_TOKEN and variable PROJECT_URL. omitted: off for new installs, kept if installed
60
+ --remove-legacy rename retired old-generation workflows to .bak so they stop running next to their replacements.
61
+ omitted: they stay and are only listed (an old deploy workflow may be your only live deployment)
31
62
  --nexus / --npm-publish (deprecated: use --publish nexus / --publish npm)
32
- --json With --mode options: machine-readable output
33
- --force, -y, --yes Skip every confirmation and use non-interactive defaults
34
- --lang en|ko Display language (default: system language, pinned to en in CI and with --force)
35
- -v, --version Print the projectops version
36
- -h, --help Show this help
37
-
38
- Examples:
39
- npx projectops --mode full --force --type spring,react
40
- npx projectops --mode workflows --type flutter --paths "flutter=app"
41
- npx projectops --mode doctor # diagnose settings
42
- GITHUB_TOKEN=ghp_... npx projectops --mode doctor # include repository settings
63
+ --force, -y, --yes skip every confirmation, use defaults for anything not given
64
+ --json with --mode options: JSON instead of text
65
+ --lang en|ko language of the CLI screen (default: system language, en in CI and with --force)
66
+ -v, --version print the projectops version
67
+ -h, --help show this help
68
+
69
+ Where results go
70
+ stderr a completion summary: files kept, retired workflows still installed, workflows not installed and why,
71
+ placeholders still to fill (__NAME__), secrets to register, next steps
72
+ .github/.projectops/logs/ why each decision was made (.log for people, .jsonl for programs)
73
+ .github/.projectops/incoming/ new template copies of files that were kept or not installed
74
+ exit code 0 = completed, 1 = invalid arguments or a failed run (the message says which)
75
+
76
+ Examples
77
+ npx projectops --mode options --json # every option, value, default and effect (read-only)
78
+ npx projectops --mode doctor # diagnose this install (read-only)
79
+ npx projectops --mode full --force # install or update everything, stack auto-detected
80
+ npx projectops --mode full --force --type spring,react # explicit stacks
81
+ npx projectops --mode workflows --force # refresh only workflows, scripts and config
82
+ npx projectops --mode workflows --force --remove-legacy # also retire old-generation workflows
83
+ npx projectops --mode full --force --coderabbit # also install the CodeRabbit review config
84
+ npx projectops --mode workflows --type flutter --paths "flutter=app" --force
85
+ GITHUB_TOKEN=ghp_... npx projectops --mode doctor # include repository settings
43
86
  `;
44
87
 
45
- const HELP_KO = `projectops — GitHub 프로젝트 자동화 템플릿 통합 CLI
88
+ const HELP_KO = `projectops — GitHub 자동화(버전 관리, 릴리스 노트, CI/CD, 이슈 템플릿, agent 스킬)를 설치·업데이트하는 CLI
46
89
 
47
90
  사용법:
48
91
  npx projectops [옵션]
49
92
 
50
- 옵션:
51
- -m, --mode MODE 통합 모드 (full | version | workflows | issues | skills | doctor | options)
52
- doctor: 통합 상태·저장소 설정 진단 (읽기 전용)
53
- options: 모든 플래그·version.yml 키 출력 (--json 이면 agent용 JSON)
54
- 기본: interactive (대화형)
55
- -t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
93
+ AI agent 나 CI 는 항상 이렇게 실행한다: npx projectops --mode <모드> --force [플래그]
94
+ 그리고 먼저 전체 옵션 표를 읽는다: npx projectops --mode options --json
95
+
96
+ 어떻게 정해지는가 (스크립트를 짜기 전에 읽는다)
97
+ - --mode 가 없거나 --mode interactive: 터미널에서 질문한다. TTY 가 필요하고 없으면 실패한다.
98
+ - 그 밖의 --mode: **아무것도 묻지 않는다.** 모든 값은 넘긴 플래그 → version.yml 에 저장된 값 → 기본값 순으로 정해진다.
99
+ TTY 가 없으면 --force 도 필요하다.
100
+ - --force: 모든 확인(호환성 변경 경고 포함)을 건너뛴다. 직접 고친 파일은 **덮어쓰지 않는다** —
101
+ 그 파일은 그대로 두고 새 템플릿 사본을 .github/.projectops/incoming/ 에 남긴다.
102
+ - 같은 명령을 두 번 돌려도 이미 최신인 것은 바꾸지 않는다.
103
+
104
+ 모드 (-m, --mode MODE)
105
+ full 전부: version.yml, 워크플로우, 스크립트, 이슈/PR 템플릿, 설정 안내서 (.coderabbit.yaml 은 --coderabbit 일 때만)
106
+ workflows .github/workflows 와 그것이 쓰는 스크립트·config·util 만. version.yml 은 다시 쓰지 않는다
107
+ version version.yml, README 버전 섹션, 버전 스크립트만
108
+ issues 이슈·디스커션 템플릿만
109
+ skills 지원 IDE 의 agent 스킬 설치·갱신
110
+ doctor 통합 상태와 저장소 설정을 읽기 전용으로 진단. 아무것도 쓰지 않는다. 문제를 찾아도 종료 코드는 0
111
+ options 모든 플래그와 version.yml 키를 언제 쓰는지·효과·기본값과 함께 출력 (--json 추가). 아무것도 쓰지 않는다
112
+ interactive 질문하며 진행 (--mode 를 안 주면 기본)
113
+
114
+ 옵션
115
+ -t, --type CSV 프로젝트 타입 csv, 첫 항목이 primary (예: spring,react,python)
56
116
  지원: spring flutter react react-native
57
117
  react-native-expo node python basic
118
+ 생략: 레포 루트의 파일로 자동 감지 (못 찾으면 basic)
58
119
  (next는 react로 흡수됨 — Next.js 프로젝트는 react 사용)
59
- --project-version V 통합 대상의 초기 버전 (예: 1.0.0). 미지정 시 자동 감지
60
- --paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
61
- --intent KIND 프로젝트 성격(#485): app | library | both | none | manual
62
- (미지정 시 --deploy/--publish에서 역추론. library/none→deploy 제외, app/none→publish 제외)
63
- --label-style STYLE 상태 라벨 표기: en (status: todo) | ko (기존 작업전)
64
- --language LANG 레포에 쓰이는 이슈/PR 템플릿 언어: en | ko
65
- --deploy TARGET 배포 방식 택1: docker-ssh(기본) | vercel | none
66
- --publish CSV publish 타겟 csv: nexus,npm,github-packages (기본: 없음)
67
- --deploy-branch NAME 릴리스 PR head 브랜치 (#456, 기본: develop). default_branch와 별개
68
- --secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
69
- --ai-summary / --no-ai-summary PR 변경 요약 워크플로우 포함/제외
70
- --projects-sync / --no-projects-sync GitHub Projects 상태 동기화 워크플로우 포함/제외 (신규 설치 기본 제외)
71
- --remove-legacy 은퇴한 구세대 워크플로우를 .bak 으로 치움 (없으면 목록만 안내)
120
+ --project-version V 초기 버전. version.yml 이 아직 없을 때만 쓰인다 (기존 값은 항상 유지)
121
+ --paths "t=p,..." 타입별 프로젝트 폴더(모노레포). 예: flutter=app,react=client. 폴더는 레포 안에 있어야 한다
122
+ --intent KIND app | library | both | none | manual. 배포·publish 질문을 어떻게 할지 정한다.
123
+ 생략: --deploy/--publish 에서 추정
124
+ --label-style STYLE en = 'status: todo' 라벨, ko = 기존 한글 이름. 신규 en, 기존 설치는 ko 유지
125
+ --language LANG en | ko. 레포에 쓰이는 이슈/PR 템플릿·봇 댓글·version.yml 주석의 언어
126
+ (CLI 화면 언어는 --lang 이다). 신규 en, 기존 설치는 저장값 유지
127
+ --deploy TARGET 앱을 어디에 배포하나, 택1: docker-ssh(SSH + Docker 서버 워크플로우) | vercel | none
128
+ 생략: 서버 스택은 docker-ssh, 모바일 타입은 무시
129
+ --publish CSV 라이브러리 레지스트리: nexus,npm,github-packages. 생략: 없음
130
+ --deploy-branch NAME **개발** 브랜치(릴리스 PR 의 head), 기본 develop. 배포가 도는 브랜치가 아니다 —
131
+ 그것은 레포 기본 브랜치다. 이 플래그는 브랜치를 만들지 않는다
132
+ --secret-backup / --no-secret-backup GitHub Secret 을 SSH 로 서버에 올린다 (SERVER_* secret 필요). 생략: 제외
133
+ --ai-summary / --no-ai-summary 작업 PR 에 AI 변경 요약 댓글. 추가 설정 없이 동작. 생략: 포함
134
+ --coderabbit / --no-coderabbit .coderabbit.yaml 설치 (--mode full 에서만). CodeRabbit GitHub 앱이 필요하다.
135
+ 이미 있으면 덮어쓰고 .bak 으로 백업. 생략: 꺼짐 (version.yml 에 켜짐이 저장돼 있으면 유지).
136
+ PR 코드 리뷰 전용이며 릴리스 노트와 무관하다
137
+ --projects-sync / --no-projects-sync GitHub Projects 보드의 Status 를 이슈 라벨과 맞춘다. secret _GITHUB_PAT_TOKEN 과
138
+ 변수 PROJECT_URL 이 필요. 생략: 신규는 제외, 이미 설치돼 있으면 유지
139
+ --remove-legacy 은퇴한 구세대 워크플로우를 .bak 으로 바꿔 신형과 함께 돌지 않게 한다.
140
+ 생략: 그대로 두고 목록만 안내한다 (옛 배포 워크플로우가 유일한 현역 배포일 수 있다)
72
141
  --nexus / --npm-publish (deprecated — --publish nexus / --publish npm 사용)
73
- --json --mode options 와 함께: 기계가 읽는 JSON 출력
74
- --force, -y, --yes 모든 확인 생략, 비대화형 기본값 사용
75
- --lang en|ko 화면 언어 (기본: 시스템 언어, CI/--force 는 en 고정)
142
+ --force, -y, --yes 모든 확인을 건너뛰고 주지 않은 값은 기본값 사용
143
+ --json --mode options 와 함께: JSON 출력
144
+ --lang en|ko CLI 화면 언어 (기본: 시스템 언어, CI/--force 는 en)
76
145
  -v, --version projectops 버전 출력
77
- -h, --help 이 도움말 표시
146
+ -h, --help 도움말
147
+
148
+ 결과는 어디에 남나
149
+ stderr 완료 요약: 유지한 파일, 아직 설치된 은퇴 워크플로우, 설치하지 않은 워크플로우와 이유,
150
+ 채워야 할 자리표시자(__NAME__), 등록할 secret, 다음 할 일
151
+ .github/.projectops/logs/ 각 결정의 이유 (.log 사람용, .jsonl 프로그램용)
152
+ .github/.projectops/incoming/ 유지했거나 설치하지 않은 파일의 새 템플릿 사본
153
+ 종료 코드 0 = 완료, 1 = 잘못된 인자 또는 실패한 실행 (메시지가 어느 쪽인지 알려 준다)
78
154
 
79
- 예시:
80
- npx projectops --mode full --force --type spring,react
81
- npx projectops --mode workflows --type flutter --paths "flutter=app"
82
- npx projectops --mode doctor # 설정 진단
83
- GITHUB_TOKEN=ghp_... npx projectops --mode doctor # 저장소 설정까지 진단
155
+ 예시
156
+ npx projectops --mode options --json # 모든 옵션·값·기본값·효과 (읽기 전용)
157
+ npx projectops --mode doctor # 이 설치를 진단 (읽기 전용)
158
+ npx projectops --mode full --force # 전부 설치·업데이트, 스택 자동 감지
159
+ npx projectops --mode full --force --type spring,react # 스택을 명시
160
+ npx projectops --mode workflows --force # 워크플로우·스크립트·config 만 갱신
161
+ npx projectops --mode workflows --force --remove-legacy # 구세대 워크플로우도 정리
162
+ npx projectops --mode full --force --coderabbit # CodeRabbit 리뷰 설정도 설치
163
+ npx projectops --mode workflows --type flutter --paths "flutter=app" --force
164
+ GITHUB_TOKEN=ghp_... npx projectops --mode doctor # 저장소 설정까지 진단
84
165
  `;
85
166
 
86
167
  export function helpText(lang = getLang()) {
@@ -6,7 +6,7 @@ import { existsSync, readFileSync } from "node:fs";
6
6
  import { writeText } from "../core/fsutil.js";
7
7
  import { PATHS } from "../core/paths.js";
8
8
  import { installAgentGuide } from "../core/agent-guide.js";
9
- import { buildVersionYml, mergeDeployValues, resolveUpdatedBy } from "../core/version-yml.js";
9
+ import { buildVersionYml, mergeDeployValues, resolveUpdatedBy, customYml } from "../core/version-yml.js";
10
10
  import { markerForType } from "../core/detect.js";
11
11
  import { addVersionSectionToReadme } from "../core/copy/readme.js";
12
12
  import { copyWorkflows } from "../core/copy/workflows.js";
@@ -51,6 +51,8 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
51
51
  buildVersionYml({
52
52
  version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
53
53
  deployValues,
54
+ // 생성기가 모르는 키(issue_helper 등)를 지킨다 — version.yml 을 매번 다시 쓰므로 안 그러면 업데이트마다 사라진다 (#835)
55
+ ...customYml(vyFile),
54
56
  updatedBy: resolveUpdatedBy(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "", targetRoot), // #811
55
57
  templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, aiPrSummary, optionsDate: today,
56
58
  changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales },
@@ -2,6 +2,7 @@
2
2
  // io 주입으로 테스트 가능. 실제 실행은 src/ui/prompts.js 함수를 io로 넘긴다.
3
3
  // 새 시각 층(banner/detectionLog/analysisCard/ideStatus/installKind/summary)과 저수준 엔진(engineIo)은
4
4
  // io의 "옵셔널 멤버" — 스텁이 생략하면 해당 층만 건너뛰고 실행 계약은 동일하다.
5
+ import { DEFAULT_CODE_REVIEW_CODERABBIT } from "../core/constants.js";
5
6
  import { join } from "node:path";
6
7
  import { existsSync, readFileSync } from "node:fs";
7
8
  import { PATHS } from "../core/paths.js";
@@ -129,7 +130,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
129
130
  let publishTargets = existing?.options?.publish ?? [];
130
131
  let includeSecretBackup = existing?.options?.secretBackup ?? false;
131
132
  let aiPrSummary = existing?.options?.aiPrSummary ?? null; // null = 아직 안 물음 (#566)
132
- let codeReviewCoderabbit = existing?.options?.codeReviewCoderabbit ?? true;
133
+ let codeReviewCoderabbit = existing?.options?.codeReviewCoderabbit ?? DEFAULT_CODE_REVIEW_CODERABBIT;
133
134
  let changelogProvider = migrateProvider(existing?.options?.changelogProvider) ?? "commit";
134
135
  let changelogBaseUrl = existing?.options?.changelogBaseUrl ?? "";
135
136
  let deployBranch = existing?.options?.deployBranch ?? "develop"; // #456
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { writeText } from "../core/fsutil.js";
7
7
  import { PATHS } from "../core/paths.js";
8
8
  import { installAgentGuide } from "../core/agent-guide.js";
9
- import { buildVersionYml, mergeDeployValues, resolveUpdatedBy } from "../core/version-yml.js";
9
+ import { buildVersionYml, mergeDeployValues, resolveUpdatedBy, customYml } from "../core/version-yml.js";
10
10
  import { markerForType } from "../core/detect.js";
11
11
  import { addVersionSectionToReadme } from "../core/copy/readme.js";
12
12
  import { applyLabelStyle } from "../core/label-style.js";
@@ -30,6 +30,8 @@ export function runVersion(context, tempDir, targetRoot = ".") {
30
30
  buildVersionYml({
31
31
  version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
32
32
  deployValues,
33
+ // 생성기가 모르는 키(issue_helper 등)를 지킨다 — version.yml 을 매번 다시 쓰므로 안 그러면 업데이트마다 사라진다 (#835)
34
+ ...customYml(vyFile),
33
35
  updatedBy: resolveUpdatedBy(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "", targetRoot), // #811
34
36
  // mode(#502): version 모드가 기존 full 통합 기록을 "version"으로 강등하지 않도록
35
37
  // 호출부가 recordMode로 기존 값을 넘긴다 (full이 우세 — 업데이트 재실행 범위 축소 방지).
@@ -21,6 +21,13 @@ export const INTENT_VALUES = Object.freeze(["app", "library", "both", "none", "m
21
21
  /** 상태 라벨 표기 — en: `status: todo`, ko: 기존 한글 `작업전` (#776) */
22
22
  export const LABEL_STYLES = Object.freeze(["en", "ko"]);
23
23
 
24
+ /**
25
+ * 저장값이 없을 때 CodeRabbit 코드 리뷰 설정을 설치할지의 기본값 (#832).
26
+ * 꺼 둔다 — CodeRabbit 은 별도 GitHub 앱을 설치해야 동작하므로, 설치 없이 켜 두면 설정 파일만 있고 아무도 리뷰하지 않는다.
27
+ * 대화형(질문하지 않는 version·issues 모드)과 비대화형이 각자 기본값을 가져 `true`/`false` 로 갈려 있던 것을 한 곳으로 모았다.
28
+ */
29
+ export const DEFAULT_CODE_REVIEW_CODERABBIT = false;
30
+
24
31
  /**
25
32
  * 릴리스 노트 생성기 (#455, #566). 기본값은 commit — 외부 의존이 없어 어디서나 결과가 나온다.
26
33
  * 실제 구현은 .github/scripts/changelog_providers/ 에 있고, 그쪽 목록과의 정합은 테스트가 본다.
@@ -13,7 +13,7 @@ import { PATHS } from "./paths.js";
13
13
  import { t as msg } from "../i18n/index.js";
14
14
  import { parseTemplateOptions, inferIntent } from "./version-yml.js";
15
15
  import { branchStatus, createBranch, pushBranch } from "./git-branch.js";
16
- import { DEPLOY_TARGETS, PUBLISH_TARGETS, CHANGELOG_PROVIDERS } from "./constants.js";
16
+ import { DEPLOY_TARGETS, PUBLISH_TARGETS, CHANGELOG_PROVIDERS, DEFAULT_CODE_REVIEW_CODERABBIT } from "./constants.js";
17
17
 
18
18
  // 개발(배포) 브랜치 존재 확인 + 생성 제안 (#477) — 대화형 전용.
19
19
  // 로컬에 없고 원격에도 없(거나 불명이)면 기본 브랜치에서 생성을 제안하고, 생성 후 push 여부도 묻는다.
@@ -336,7 +336,7 @@ export async function askAllOptionalWorkflows({
336
336
  if (force || !tty || typeof io.select !== "function") {
337
337
  // 비대화형 기본값: CodeRabbit은 별도 앱 설치가 있어야 실제로 동작하므로
338
338
  // 자동화 환경에서 켜봐야 의미가 없다. 설정 없이 바로 도는 요약만 켠다.
339
- codeReviewCoderabbit = codeReviewCoderabbit ?? false;
339
+ codeReviewCoderabbit = codeReviewCoderabbit ?? DEFAULT_CODE_REVIEW_CODERABBIT;
340
340
  aiPrSummary = aiPrSummary ?? true;
341
341
  } else {
342
342
  say("");
@@ -58,6 +58,9 @@ export const FLAGS = [
58
58
  { flag: "--ai-summary", negation: "--no-ai-summary",
59
59
  description: "Include / exclude the workflow that comments an AI summary on work PRs.", default: "included",
60
60
  versionYml: "metadata.template.options.code_review.ai_summary" },
61
+ { flag: "--coderabbit", negation: "--no-coderabbit",
62
+ description: "Install the CodeRabbit code review config (.coderabbit.yaml). Only --mode full installs it.",
63
+ default: "off unless version.yml already says on", versionYml: "metadata.template.options.code_review.coderabbit" },
61
64
  { flag: "--projects-sync", negation: "--no-projects-sync",
62
65
  description: "Include / exclude the GitHub Projects status sync workflow. Needs a PAT secret and PROJECT_URL variable.",
63
66
  default: "excluded for new installs; kept if already installed", versionYml: "metadata.template.options.projects_sync" },
@@ -116,6 +119,200 @@ export const VERSION_YML = [
116
119
  { path: "metadata.template.options.changelog.base_url", parserKey: "changelogBaseUrl", type: "string", description: "Only for provider ollama.", edit: "by hand" },
117
120
  ];
118
121
 
122
+ // ── agent 가 읽고 스스로 판단하게 하는 상세 설명 ───────────────────────────────────────────────
123
+ // 짧은 description 만으로는 "언제 쓰나 · 쓰면 레포에서 정확히 무엇이 바뀌나 · 안 쓰면 어떻게 되나"를 알 수 없다.
124
+ // 사람 없이 이 CLI 를 부르는 AI agent 가 추측하지 않도록 항목마다 적는다.
125
+ // when : 이 값을 직접 정해야 하는 상황 / 그냥 둬도 되는 상황
126
+ // effect : 레포에서 실제로 바뀌는 것 (어떤 파일이 생기고 사라지는지, 어떤 워크플로가 도는지)
127
+ // omitted : 안 주면 어떻게 정해지는가
128
+ // example : 그대로 실행할 수 있는 명령
129
+ // caution : 되돌리기 어렵거나 놓치기 쉬운 것
130
+ // test/options-schema.test.js 가 모든 (폐기 아닌) 플래그·옵션에 when·effect 가 있는지 검사한다 — 새 항목은 여기에도 적는다.
131
+ export const FLAG_DETAILS = {
132
+ "--mode": {
133
+ when: "Always pass it explicitly from an agent or CI. Without it the CLI opens interactive questions.",
134
+ effect: "Chooses which parts are installed. full = everything; workflows = only .github/workflows (+ scripts, config, util); version = only version.yml + README version section; issues = only issue/PR templates; skills = IDE agent skills; doctor and options never write anything.",
135
+ omitted: "interactive: asks questions in the terminal and fails without a TTY.",
136
+ example: "npx projectops --mode full --force",
137
+ caution: "Any mode other than interactive decides every option from flags, version.yml and defaults — it does not ask. Without a TTY, --force is also required.",
138
+ },
139
+ "--type": {
140
+ when: "Pass it when auto-detection is wrong or the repo has several stacks (a monorepo).",
141
+ effect: "Selects which project-types/<type>/ workflows are installed and which build file version_manager keeps in sync (pubspec.yaml, package.json, build.gradle, pyproject.toml ...).",
142
+ omitted: "Detected from marker files in the repo root (pubspec.yaml → flutter, build.gradle → spring, package.json → node/react ...). Nothing detected → basic (version.yml only).",
143
+ example: "npx projectops --mode full --force --type spring,react",
144
+ caution: "The first type is the primary one. Do not change types after setup.",
145
+ },
146
+ "--project-version": {
147
+ when: "Only for a repo that has no version.yml yet and where the detected version is wrong.",
148
+ effect: "Sets the starting version written into version.yml.",
149
+ omitted: "Read from the build file (pubspec.yaml, package.json ...); an existing version.yml value is always kept.",
150
+ example: "npx projectops --mode full --force --project-version 1.0.0",
151
+ },
152
+ "--paths": {
153
+ when: "Monorepos where a stack lives in a subfolder (app/, client/ ...).",
154
+ effect: "Writes project_paths into version.yml; version_manager then syncs the version file inside that folder and workflow path filters point at it.",
155
+ omitted: "Every type is assumed to be at the repo root.",
156
+ example: 'npx projectops --mode full --force --type flutter,react --paths "flutter=app,react=client"',
157
+ caution: "Each folder must exist inside the repo. Absolute paths and '..' are rejected.",
158
+ },
159
+ "--intent": {
160
+ when: "Rarely needed. It only matters for interactive questions and for deriving --deploy / --publish.",
161
+ effect: "app → deploy asked, publish skipped; library → publish asked, deploy forced to none; both → both asked; none → neither; manual → both asked one by one. Stored as options.intent.",
162
+ omitted: "Inferred from --deploy and --publish (or from the stored version.yml values).",
163
+ example: "npx projectops --mode full --force --intent library --publish npm",
164
+ },
165
+ "--deploy": {
166
+ when: "Set it when the app is deployed by this repo. Library repos use --publish instead.",
167
+ effect: "docker-ssh installs the SSH + Docker server deploy workflows (SIMPLE-CICD, NONSTOP-*, PR-PREVIEW for spring/python; CICD for react). vercel installs the Vercel deploy workflow. none installs no deploy workflow. Mobile types (flutter, react-native*) ignore it — their store deploy workflows are part of the type.",
168
+ omitted: "docker-ssh for server stacks, none for mobile apps and library repos.",
169
+ example: "npx projectops --mode full --force --type spring --deploy vercel",
170
+ caution: "docker-ssh workflows need server secrets (SERVER_HOST, SERVER_USER, SERVER_PASSWORD or SSH_KEY, DOCKERHUB_*). Until they are registered the workflows fail — the completion summary lists the missing secrets.",
171
+ },
172
+ "--publish": {
173
+ when: "Set it when the repo is a library published to a package registry.",
174
+ effect: "nexus installs NEXUS-CI and NEXUS-PUBLISH (spring); npm installs NODE-NPM-PUBLISH (needs NPM_TOKEN); github-packages installs GITHUB-PACKAGES-PUBLISH (spring). Several values are allowed.",
175
+ omitted: "No publish workflow.",
176
+ example: "npx projectops --mode workflows --force --type node --publish npm",
177
+ },
178
+ "--deploy-branch": {
179
+ when: "Only if your development branch is not named develop.",
180
+ effect: "Writes metadata.deploy_branch and rewrites the branch name inside the installed workflows (release PR head, CI triggers, PR summary base). The branch is NOT created by this flag.",
181
+ omitted: "develop.",
182
+ example: "npx projectops --mode full --force --deploy-branch dev",
183
+ caution: "Despite the name this is the development branch (head of the release PR), not the branch that deploys. The deploying branch is the repository default branch.",
184
+ },
185
+ "--language": {
186
+ when: "Set it only to change the language of an existing repo or to start a new repo in Korean.",
187
+ effect: "Chooses the language of the issue/PR templates and bot comments written into the repo, and of the comments inside version.yml.",
188
+ omitted: "New installs: en. Existing installs keep the stored value (ko if none was stored).",
189
+ example: "npx projectops --mode full --force --language ko",
190
+ caution: "This is not the language of the CLI screen — that is --lang.",
191
+ },
192
+ "--label-style": {
193
+ when: "Only when the repo already uses one label naming and you must keep it.",
194
+ effect: "Writes the label definitions (.github/config/issue-labels.yml) and the label names used inside the installed workflows in that style: en = 'status: todo', 'status: done'; ko = the legacy Korean names (작업전, 작업완료). Workflows and skills accept both. Labels are created in GitHub by running the PROJECT-COMMON-SYNC-ISSUE-LABELS workflow once.",
195
+ omitted: "New installs: en. Existing installs keep ko unless a value is stored.",
196
+ example: "npx projectops --mode full --force --label-style ko",
197
+ },
198
+ "--secret-backup": {
199
+ when: "Only if you want GitHub Secrets copied to your own server over SSH.",
200
+ effect: "Installs PROJECT-COMMON-SECRET-FILE-UPLOAD, which runs on pushes to the development branch and needs SERVER_HOST, SERVER_USER and SERVER_PASSWORD.",
201
+ omitted: "Not installed.",
202
+ example: "npx projectops --mode workflows --force --secret-backup",
203
+ },
204
+ "--ai-summary": {
205
+ when: "Leave the default unless you want no automatic PR comment.",
206
+ effect: "Installs PROJECT-COMMON-AI-PR-SUMMARY, which comments a change summary on work PRs into the development branch. It works with no extra setup; registering an AI key (GEMINI_API_KEY, GROQ_API_KEY, ...) makes the wording better.",
207
+ omitted: "Installed (or kept as stored).",
208
+ example: "npx projectops --mode workflows --force --no-ai-summary",
209
+ },
210
+ "--coderabbit": {
211
+ when: "Pass --coderabbit only if the team uses CodeRabbit for pull request code review.",
212
+ effect: "Copies .coderabbit.yaml to the repo root. Only --mode full installs it. If the file already exists it is overwritten and the old one is saved as .coderabbit.yaml.bak. This is the PR review bot only; release notes are generated separately (PR body → AI key → commit analysis) and do not need it.",
213
+ omitted: "Off, unless version.yml already stores code_review.coderabbit: true (a stored choice is kept). The wizard asks interactively; non-interactive runs never turn it on by themselves.",
214
+ example: "npx projectops --mode full --force --coderabbit",
215
+ caution: "CodeRabbit needs its GitHub app installed and granted access to the repo (https://coderabbit.ai). Without the app the file does nothing and no review comments appear.",
216
+ },
217
+ "--projects-sync": {
218
+ when: "Only if the repo uses a GitHub Projects board whose Status column should follow issue labels.",
219
+ effect: "Installs PROJECT-COMMON-PROJECTS-SYNC-MANAGER. It needs the secret _GITHUB_PAT_TOKEN and the repository variable PROJECT_URL; without them it fails on every label change.",
220
+ omitted: "Not installed for new installs; kept if it is already installed.",
221
+ example: "npx projectops --mode workflows --force --projects-sync",
222
+ },
223
+ "--remove-legacy": {
224
+ when: "After an update printed 'retired workflows are still installed' (or doctor listed them) and the new workflow is working.",
225
+ effect: "Renames retired old-generation workflows (for example PROJECT-SPRING-SYNOLOGY-*, PROJECT-COMMON-RELEASE-PUBLISH) to *.yaml.bak so they stop running. Delete the .bak to restore.",
226
+ omitted: "Retired workflows are left in place and only listed in the completion summary and in doctor. Plain renames that have an identical replacement are neutralised automatically; deploy pipelines are never touched without this flag.",
227
+ example: "npx projectops --mode workflows --force --remove-legacy",
228
+ caution: "Old deploy workflows may be the repo's only live deployment. Check that the replacement works first.",
229
+ },
230
+ "--force": {
231
+ when: "Always, from an agent or CI.",
232
+ effect: "Skips every confirmation (including the breaking-changes warning) and uses defaults for anything not given as a flag. Files you modified are NOT overwritten: they are kept and a copy of the new template is saved in .github/.projectops/incoming/ for you to diff.",
233
+ omitted: "Confirmation prompts appear when a TTY is attached; without a TTY the command stops with an error asking for --force.",
234
+ example: "npx projectops --mode workflows --force",
235
+ caution: "Idempotent: running it again changes nothing that is already current.",
236
+ },
237
+ "--lang": {
238
+ when: "Rarely. Only to force the language of the terminal output.",
239
+ effect: "Changes the language of CLI messages (not of files written into the repo).",
240
+ omitted: "System language; en in CI and whenever --force is given.",
241
+ example: "npx projectops --mode doctor --lang ko",
242
+ },
243
+ "--json": {
244
+ when: "With --mode options, whenever a program or agent reads the output.",
245
+ effect: "Prints the full option table as JSON instead of text. No files are written.",
246
+ omitted: "Human-readable text.",
247
+ example: "npx projectops --mode options --json",
248
+ },
249
+ "--version": { when: "To learn which projectops release is running.", effect: "Prints the package version and exits. Writes nothing.", omitted: "-", example: "npx projectops --version" },
250
+ "--help": { when: "To read the short human help.", effect: "Prints the help text and exits. Writes nothing. For agents, --mode options --json is more complete.", omitted: "-", example: "npx projectops --help" },
251
+ };
252
+
253
+ export const KEY_DETAILS = {
254
+ "version": { when: "Read it to know the current version. Edit by hand only when asked to change the version.", effect: "version_manager syncs this value into pubspec.yaml / package.json / build.gradle / pyproject.toml ...", omitted: "Created from the build file at install." },
255
+ "version_code": { when: "Do not edit.", effect: "Build number. +1 on every release; used by Android release builds (iOS and test builds use build_number.py).", omitted: "1" },
256
+ "project_types": { when: "Set by the wizard. Do not change after setup.", effect: "Decides which type workflows run and which build files are synced. The first entry is primary.", omitted: "Detected at install." },
257
+ "project_paths": { when: "Monorepos only.", effect: "Maps a type to its subfolder so version sync and workflow path filters look there.", omitted: "Every type at the repo root." },
258
+ "metadata.default_branch": { when: "Do not edit unless the repository default branch was renamed.", effect: "The branch whose pushes deploy and publish. Workflows trigger on it.", omitted: "main" },
259
+ "metadata.deploy_branch": { when: "Only if the development branch is not develop.", effect: "Head of the release PR; workflows rewrite their develop references to this name.", omitted: "develop", caution: "Not the deploying branch, despite the name." },
260
+ "metadata.template.options.intent": { when: "Leave as is.", effect: "Remembers the project kind so a re-run does not ask the deploy questions again.", omitted: "Inferred from deploy and publish." },
261
+ "metadata.template.options.deploy": { when: "Edit via --deploy, not by hand.", effect: "Which deploy workflow family is installed on the next update (docker-ssh, vercel, none).", omitted: "docker-ssh for server stacks." },
262
+ "metadata.template.options.publish": { when: "Edit via --publish.", effect: "Which registry publish workflows are installed on the next update.", omitted: "[]" },
263
+ "metadata.template.options.secret_backup": { when: "Edit via --secret-backup.", effect: "Whether the secret backup workflow is installed.", omitted: "false" },
264
+ "metadata.template.options.semver_auto": { when: "Set false only if you want every release to be a patch bump.", effect: "true: the release PR and direct pushes to the default branch pick the bump from commit titles ('type!:' major, 'feat:' minor, otherwise patch). false: always patch.", omitted: "true (also for existing installs when the key is missing).", caution: "A repo that switches from patch-only to true can jump a minor version on its next release; the update summary warns about it." },
265
+ "metadata.template.options.close_on_release": { when: "Set false to keep finished issues open after a release.", effect: "A release merge closes issues that carry the done label and are referenced by a release commit.", omitted: "true for new installs; off (key missing) for existing installs." },
266
+ "metadata.template.options.language": { when: "Edit via --language.", effect: "Language of issue/PR templates, bot comments and version.yml comments.", omitted: "en for new installs, ko for existing ones." },
267
+ "metadata.template.options.label_style": { when: "Edit via --label-style.", effect: "Status label names (en 'status: todo' or legacy ko).", omitted: "en for new installs, ko for existing ones." },
268
+ "metadata.template.options.projects_sync": { when: "Edit via --projects-sync.", effect: "Whether the Projects status sync workflow is installed.", omitted: "false for new installs; kept if installed." },
269
+ "metadata.template.options.app_release": { when: "Set true by hand for repos whose releases go through App Store / Play Store review.", effect: "The deploy skill (pro-changelog-deploy) treats releases as going through store review: release notes get a review warning banner and are checked more strictly, and the release PR always waits for manual approval instead of automerging.", omitted: "Not set. Never enabled automatically.", caution: "Do not set it on repos that do not ship to a store." },
270
+ "metadata.template.options.excluded_workflows": { when: "Add a file name to stop the update from ever installing that template workflow.", effect: "Listed files are skipped on install and update. Files you deleted yourself are also not restored; a copy of the template is kept in .github/.projectops/incoming/.", omitted: "[]", example: 'excluded_workflows: ["PROJECT-COMMON-TEMPLATE-UTIL-VERSION-SYNC.yml"]' },
271
+ "metadata.template.options.store_locales": { when: "Set it when the store listing has several languages and release notes must not be one Korean text copied into all of them.", effect: "Turns on per-language store release notes (Play Console 'Release notes', App Store 'What's New'). The first entry is the default language. Each other language uses the text in store_notes[<language>] of that version in CHANGELOG.json; a language without text gets the default-language text, because the stores reject an empty What's New.", omitted: "Off: the same text is used for every store language, as before.", example: 'store_locales: ["ko-KR", "en-US", "ja-JP", "zh-CN"]' },
272
+ "metadata.template.options.code_review.coderabbit": { when: "Edit via --coderabbit / --no-coderabbit.", effect: "Whether .coderabbit.yaml is installed by --mode full. PR review bot only; unrelated to release notes.", omitted: "false" },
273
+ "metadata.template.options.code_review.ai_summary": { when: "Edit via --ai-summary.", effect: "Whether the AI PR summary workflow is installed.", omitted: "true" },
274
+ "metadata.template.options.changelog.provider": { when: "Leave the default. Set only to force one generator.", effect: "Release notes: the PR body is used as is when it already has notes; otherwise a registered AI key (GEMINI_API_KEY, GROQ_API_KEY, MISTRAL_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY) is used; otherwise the commit analysis, which always works and costs nothing. 'copilot' consumes Copilot premium requests, so it is only used when set here.", omitted: "commit (an AI key is still picked up automatically)." },
275
+ "metadata.template.options.changelog.base_url": { when: "Only with provider ollama.", effect: "Base URL of the Ollama server (OpenAI-compatible, ending in /v1).", omitted: "" },
276
+ };
277
+
278
+ // 자주 하는 일 → 그대로 실행할 명령. agent 가 옵션을 조합하다 틀리지 않게 한다.
279
+ export const RECIPES = [
280
+ { goal: "Install everything in a new repo (stack auto-detected)", command: "npx projectops --mode full --force", note: "Add --type spring,react for several stacks." },
281
+ { goal: "Update an existing install to the newest templates", command: "npx projectops --mode full --force", note: "Files you modified are kept; read .github/.projectops/incoming/ and the completion summary for what was skipped." },
282
+ { goal: "Refresh only the workflows, scripts and config", command: "npx projectops --mode workflows --force", note: "version.yml is not rewritten." },
283
+ { goal: "See the state of an install and the repository settings (read-only)", command: "npx projectops --mode doctor", note: "Add GITHUB_TOKEN=... to also check repository settings." },
284
+ { goal: "Read every option with values, defaults and effects", command: "npx projectops --mode options --json" },
285
+ { goal: "Install the CodeRabbit PR review config", command: "npx projectops --mode full --force --coderabbit", note: "Needs the CodeRabbit GitHub app; overwrites an existing .coderabbit.yaml (backup saved as .bak)." },
286
+ { goal: "Never install a template workflow again", command: "Add its file name to metadata.template.options.excluded_workflows in version.yml", note: "Delete the installed file too; the update will not bring it back." },
287
+ { goal: "Stop retired workflows from running next to their replacements", command: "npx projectops --mode workflows --force --remove-legacy", note: "Run only after the new workflow is working." },
288
+ { goal: "Keep every release a patch bump", command: "Set metadata.template.options.semver_auto: false in version.yml" },
289
+ { goal: "Install a library publish workflow", command: "npx projectops --mode workflows --force --type node --publish npm", note: "Needs the NPM_TOKEN secret." },
290
+ ];
291
+
292
+ // 이 CLI 를 사람 없이 쓰는 agent 가 지킬 규칙
293
+ export const AGENT_RULES = [
294
+ "Always pass an explicit --mode and --force. Without --force and without a TTY the command stops; with --mode interactive it needs a human.",
295
+ "Read before you write: run --mode doctor (state) and --mode options --json (this table). Both are read-only and need no network.",
296
+ "Only flags you pass, the stored version.yml values and the defaults decide anything — nothing is asked. Check the 'omitted' text of a flag before relying on its default.",
297
+ "Your edits are safe: a workflow you changed is kept as is, and a copy of the new template goes to .github/.projectops/incoming/. Compare with: diff -u .github/workflows/<file> .github/.projectops/incoming/<file>.",
298
+ "After a run read the completion summary on stderr: it lists kept files, retired workflows still installed, workflows not installed, placeholders that still need a value (__NAME__) and secrets that must be registered.",
299
+ "Every run is recorded in .github/.projectops/logs/ (.log for people, .jsonl for programs). Read the newest .log to see why something was decided. Never edit those files.",
300
+ "Exit code 0 means the run completed (doctor also returns 0 when it finds things to look at). 1 means invalid arguments or a failed run; the message says which.",
301
+ "Never delete .github/.projectops/baseline.json: it is how the next update tells your edits from template changes.",
302
+ ];
303
+
304
+ export const OUTPUT_FILES = [
305
+ { path: "version.yml", written_by: "full, version", note: "Regenerated on every full/version run from the stored values plus flags. Options and top-level keys the wizard does not generate itself (for example options.issue_helper) are kept verbatim, comments included; legacy keys (nexus, npm_publish, synology, project_type) are converted to their new form and not kept; a malformed block is dropped rather than copied. workflows mode does not touch version.yml." },
306
+ { path: ".github/workflows/PROJECT-*.yaml", written_by: "full, workflows", note: "Installed per type and options. A file you modified is kept." },
307
+ { path: ".github/scripts/, .github/config/, .github/util/", written_by: "full, workflows", note: "Helper scripts used by the workflows." },
308
+ { path: ".github/ISSUE_TEMPLATE/, PULL_REQUEST_TEMPLATE.md", written_by: "full, issues", note: "Language follows options.language." },
309
+ { path: ".coderabbit.yaml", written_by: "full (only with --coderabbit)", note: "Overwritten with a .bak backup if present." },
310
+ { path: ".github/.projectops/AGENT-GUIDE.md", written_by: "full, version, workflows", note: "Short guide for AI agents working in the repo. Regenerated on every update." },
311
+ { path: ".github/.projectops/baseline.json", written_by: "full, workflows", note: "Hashes of what was installed. Tracked by git; do not delete." },
312
+ { path: ".github/.projectops/incoming/", written_by: "full, workflows", note: "New template copies of files that were kept or not installed. Not tracked by git." },
313
+ { path: ".github/.projectops/logs/", written_by: "full, workflows", note: "Run log and event trace. Not tracked by git." },
314
+ ];
315
+
119
316
  export function buildSchema(packageVersion = "") {
120
317
  return {
121
318
  tool: "projectops",
@@ -123,8 +320,11 @@ export function buildSchema(packageVersion = "") {
123
320
  schemaVersion: SCHEMA_VERSION,
124
321
  usage: "npx projectops --mode <mode> [--type csv] [--force] ... (agents: always pass --force and an explicit --mode)",
125
322
  modes: MODES,
126
- flags: FLAGS,
127
- versionYml: VERSION_YML,
323
+ flags: FLAGS.map((f) => ({ ...f, ...(FLAG_DETAILS[f.flag] || {}) })),
324
+ versionYml: VERSION_YML.map((k) => ({ ...k, ...(KEY_DETAILS[k.path] || {}) })),
325
+ agentRules: AGENT_RULES,
326
+ recipes: RECIPES,
327
+ outputFiles: OUTPUT_FILES,
128
328
  related: {
129
329
  diagnose: "npx projectops --mode doctor",
130
330
  optionsJson: "npx projectops --mode options --json",
@@ -137,17 +337,30 @@ export function buildSchema(packageVersion = "") {
137
337
  export function renderText(schema) {
138
338
  const lines = [`projectops ${schema.packageVersion} — options (schema v${schema.schemaVersion})`, "", schema.usage, "", "MODES"];
139
339
  for (const m of schema.modes) lines.push(` ${m.name.padEnd(12)} ${m.description}`);
340
+ const detail = (o) => {
341
+ for (const [label, key] of [["when", "when"], ["effect", "effect"], ["if omitted", "omitted"], ["caution", "caution"], ["example", "example"]]) {
342
+ if (o[key]) lines.push(` ${label}: ${o[key]}`);
343
+ }
344
+ };
345
+ lines.push("", "RULES FOR AGENTS");
346
+ schema.agentRules.forEach((r, i) => lines.push(` ${i + 1}. ${r}`));
347
+ lines.push("", "RECIPES");
348
+ for (const r of schema.recipes) lines.push(` ${r.goal}`, ` ${r.command}`, ...(r.note ? [` (${r.note})`] : []));
140
349
  lines.push("", "FLAGS");
141
350
  for (const f of schema.flags) {
142
351
  const name = [...(f.aliases ?? [f.alias]), f.flag + (f.value ? ` ${f.value}` : ""), f.negation].filter(Boolean).join(", ");
143
352
  const desc = f.deprecated ? `(deprecated) ${f.deprecated}` : f.description;
144
353
  const extra = [f.values ? `values: ${f.values.join("|")}` : "", f.default !== undefined ? `default: ${f.default}` : ""].filter(Boolean).join("; ");
145
354
  lines.push(` ${name}`, ` ${desc}${extra ? ` [${extra}]` : ""}`);
355
+ detail(f);
146
356
  }
147
357
  lines.push("", "VERSION.YML KEYS");
148
358
  for (const k of schema.versionYml) {
149
359
  const extra = [k.type, k.values ? k.values.join("|") : "", k.default !== undefined ? `default: ${JSON.stringify(k.default)}` : ""].filter(Boolean).join("; ");
150
360
  lines.push(` ${k.path} [${extra}]`, ` ${k.description}`);
361
+ detail(k);
151
362
  }
363
+ lines.push("", "FILES WRITTEN");
364
+ for (const o of schema.outputFiles) lines.push(` ${o.path} (${o.written_by})`, ` ${o.note}`);
152
365
  return lines.join("\n");
153
366
  }
@@ -2,6 +2,7 @@
2
2
  // ⚠️ YAML 재직렬화 금지 — 주석이 데이터. .sh heredoc과 바이트 동일한 템플릿 문자열.
3
3
  // 실측 기준: template_integrator.sh 2184~2354.
4
4
  import { execFileSync } from "node:child_process";
5
+ import { readFileSync } from "node:fs";
5
6
  import { DEPLOY_TARGETS, LABEL_STYLES } from "./constants.js";
6
7
  import { REPO_LANGUAGES } from "./repo-language.js";
7
8
 
@@ -406,6 +407,86 @@ export function parseExisting(content) {
406
407
  return { version, versionCode, types, paths, templateVersion, templateMode, options, defaultBranch, deployBranch };
407
408
  }
408
409
 
410
+ // ── 생성기가 모르는 키를 지키기 (#835) ─────────────────────────────────────────────
411
+ // version.yml 은 매 full/version 실행마다 처음부터 다시 쓴다. 생성기가 아는 키만 쓰면 사용자가 넣은 것은 사라진다 —
412
+ // 특히 문서화된 `options.issue_helper`(브랜치 접두사·커밋 템플릿·시간대 …)가 업데이트마다 조용히 초기화됐다.
413
+ // 그래서 아는 키는 지금처럼 만들고, 모르는 키는 **글자 그대로**(들여쓰기·주석 포함) 되돌려 쓴다.
414
+ // project_type(단수)는 v4.1.0 에서 없앴다 — 남기면 version_manager 가 거부한다. 그래서 보존하지 않고 버린다.
415
+ const OWNED_TOP = new Set(["version", "version_code", "project_types", "project_type", "project_paths", "metadata", "deploy"]);
416
+ // metadata.template.options 아래에서 생성기가 직접 쓰는 키. 여기에 없는 것만 보존한다.
417
+ // nexus · npm_publish · synology 는 옛 키다 — 파서가 신형(publish · secret_backup)으로 바꿔 쓰므로 그대로 두면 이중 기록이 된다.
418
+ const OWNED_OPTION = new Set(["intent", "deploy", "publish", "secret_backup", "semver_auto", "projects_sync", "close_on_release",
419
+ "language", "label_style", "app_release", "excluded_workflows", "store_locales", "code_review", "changelog",
420
+ "nexus", "npm_publish", "synology"]);
421
+
422
+ // indent 칸으로 시작하는 키 한 줄과 그 자식(더 깊은 들여쓰기·빈 줄·그 아래의 주석)을 한 덩어리로 자른다.
423
+ function keyBlocks(lines, indent, isOwned) {
424
+ const keyRe = new RegExp(`^ {${indent}}([A-Za-z_][\\w-]*):`);
425
+ const blocks = [];
426
+ for (let i = 0; i < lines.length; i++) {
427
+ const m = lines[i].match(keyRe);
428
+ if (!m) continue;
429
+ let j = i + 1;
430
+ while (j < lines.length && (lines[j].trim() === "" || /^\s/.test(lines[j]) && lines[j].search(/\S/) > indent
431
+ || (lines[j].trim().startsWith("#") && lines[j].search(/\S/) > indent))) j++;
432
+ while (j > i + 1 && lines[j - 1].trim() === "") j--; // 덩어리 끝의 빈 줄은 뺀다
433
+ const block = lines.slice(i, j).join("\n");
434
+ if (!isOwned(m[1]) && looksWellFormed(block)) blocks.push(block);
435
+ i = j - 1;
436
+ }
437
+ return blocks;
438
+ }
439
+
440
+ // 되돌려 쓸 덩어리가 깨진 YAML 이면 version.yml 전체가 깨진다 — 그러면 모든 워크플로가 버전을 못 읽는다.
441
+ // 의존성 없이 확인할 수 있는 만큼만 본다: 괄호·중괄호 짝, 닫히지 않은 따옴표. 의심스러우면 버린다(보존보다 파일 무결성이 먼저).
442
+ function looksWellFormed(block) {
443
+ const text = block.split("\n").map((l) => l.replace(/\s#.*$/, "")).join("\n"); // 줄 끝 주석은 짝 검사에서 뺀다
444
+ const stack = [];
445
+ const pairs = { "]": "[", "}": "{" };
446
+ let quote = null;
447
+ for (const ch of text) {
448
+ if (quote) { if (ch === quote) quote = null; continue; }
449
+ if (ch === '"' || ch === "'") { quote = ch; continue; }
450
+ if (ch === "[" || ch === "{") stack.push(ch);
451
+ else if (ch === "]" || ch === "}") { if (stack.pop() !== pairs[ch]) return false; }
452
+ }
453
+ return !quote && stack.length === 0;
454
+ }
455
+
456
+ /** 기존 version.yml 에서 생성기가 모르는 것만 뽑는다. { optionBlocks: string[], topBlocks: string[] } */
457
+ export function extractCustomYml(content) {
458
+ const lines = String(content || "").split(/\r?\n/);
459
+ const topBlocks = keyBlocks(lines, 0, (k) => OWNED_TOP.has(k));
460
+ // metadata → template → options 구간만 잘라 6칸 들여쓴 키를 본다
461
+ const find = (from, to, re) => { for (let i = from; i < to; i++) if (re.test(lines[i])) return i; return -1; };
462
+ const end = (start, indent) => { let j = start + 1; while (j < lines.length && (lines[j].trim() === "" || lines[j].search(/\S/) > indent)) j++; return j; };
463
+ const md = find(0, lines.length, /^metadata:/);
464
+ let optionBlocks = [];
465
+ if (md >= 0) {
466
+ const mdEnd = end(md, 0);
467
+ const tp = find(md + 1, mdEnd, /^ {2}template:/);
468
+ if (tp >= 0) {
469
+ const tpEnd = end(tp, 2);
470
+ const op = find(tp + 1, tpEnd, /^ {4}options:/);
471
+ if (op >= 0) optionBlocks = keyBlocks(lines.slice(op + 1, end(op, 4)), 6, (k) => OWNED_OPTION.has(k));
472
+ }
473
+ }
474
+ return { optionBlocks, topBlocks };
475
+ }
476
+
477
+ /**
478
+ * buildVersionYml 에 그대로 펼쳐 넣는 도우미: `...customYml(경로)`.
479
+ * 파일이 없거나 읽을 수 없으면 아무것도 보존하지 않는다 — 읽기 실패가 업데이트를 막으면 안 된다.
480
+ */
481
+ export function customYml(file) {
482
+ try {
483
+ const { optionBlocks, topBlocks } = extractCustomYml(readFileSync(file, "utf8"));
484
+ return { customOptionBlocks: optionBlocks, customTopBlocks: topBlocks };
485
+ } catch {
486
+ return {};
487
+ }
488
+ }
489
+
409
490
  // 기존 version.yml 의 deploy 블록 → Map<type, Map<key,value>> (#670 — 재실행 시 사용자 수정값 보존용).
410
491
  // 4.28.0 이 만든 깨진 구조(deploy 아래에 2칸 들여쓴 template 이 딸려 있음)도 읽을 수 있게,
411
492
  // 2칸 키 `template` 은 타입이 아니라 블록 종료로 본다.
@@ -444,7 +525,7 @@ export function mergeDeployValues(existingContent, fresh) {
444
525
  // today = "YYYY-MM-DD" (UTC)
445
526
  // pathMarkers = Map<type, markerFilename> (project_paths 주석용)
446
527
  // templateOptions = { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate } (template 블록)
447
- export function buildVersionYml({ version, types = [], paths = new Map(), pathMarkers = new Map(), branch = "main", deployBranch = "", versionCode = 1, now, today, templateOptions = null, deployValues = new Map(), updatedBy = "template_integrator" }) {
528
+ export function buildVersionYml({ version, types = [], paths = new Map(), pathMarkers = new Map(), branch = "main", deployBranch = "", versionCode = 1, now, today, templateOptions = null, deployValues = new Map(), updatedBy = "template_integrator", customOptionBlocks = [], customTopBlocks = [] }) {
448
529
  const lang = templateOptions?.language === "ko" ? "ko" : "en";
449
530
  const C = commentsFor(lang);
450
531
  const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(",")}]` : `["basic"]`;
@@ -518,6 +599,8 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
518
599
  out += ` changelog:\n`;
519
600
  out += ` provider: "${changelogProvider}"\n`;
520
601
  out += ` base_url: "${changelogBaseUrl}"\n`;
602
+ // 생성기가 모르는 옵션(issue_helper 등)은 그대로 되돌려 쓴다 (#835)
603
+ for (const b of customOptionBlocks) out += `${b}\n`;
521
604
  }
522
605
 
523
606
  // deploy 블록 (.sh update_version_yml_deploy). deployValues: Map<type, Map<key,value>>.
@@ -532,5 +615,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
532
615
  for (const [k, v] of deployValues.get(t)) out += ` ${k}: "${v}"\n`;
533
616
  }
534
617
  }
618
+ // 최상위의 모르는 키도 그대로 (#835)
619
+ for (const b of customTopBlocks) out += `\n${b}\n`;
535
620
  return out;
536
621
  }
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ import { createRunTrace, MIGRATION_DIR } from "./core/run-trace.js";
20
20
  import { appendGuideEntry } from "./core/migration-guide.js";
21
21
  import { resolveProjectPaths, markerForType } from "./core/paths-resolve.js";
22
22
  import { applicableTargets, migrateProvider } from "./core/options-ask.js";
23
+ import { DEFAULT_CODE_REVIEW_CODERABBIT } from "./core/constants.js";
23
24
  import { printBannerCompact } from "./ui/banner.js";
24
25
  import { printSummary } from "./ui/summary.js";
25
26
  import { runFull } from "./commands/full.js";
@@ -252,7 +253,8 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
252
253
  // changelog/code_review 축(#455): 비대화형은 저장값 → 기본값. null이 흘러 provider:"null"로 기록되던 버그 수정.
253
254
  changelogProvider: migrateProvider(existing?.options?.changelogProvider) ?? "commit",
254
255
  changelogBaseUrl: existing?.options?.changelogBaseUrl ?? "",
255
- codeReviewCoderabbit: existing?.options?.codeReviewCoderabbit ?? false,
256
+ // 플래그 > 저장값 > 기본(꺼짐). 쓰겠다고 명시하면 켜지고, 아무 말이 없을 때만 기본값이 적용된다.
257
+ codeReviewCoderabbit: opts.codeReviewCoderabbit ?? existing?.options?.codeReviewCoderabbit ?? DEFAULT_CODE_REVIEW_CODERABBIT,
256
258
  // semver 자동 승격(#546, #814): 저장값(명시적 false 포함)은 존중하고, 키가 없으면 신규·기존 모두 켠다.
257
259
  // 기존 레포에서 처음 켜질 때는 버전이 예고 없이 오르지 않게 완료 화면이 알린다 (semverAutoNewlyOn).
258
260
  semverAuto: existing?.options?.semverAuto ?? true,