projectops 4.31.4 → 4.33.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.
Files changed (57) hide show
  1. package/README.md +144 -145
  2. package/package.json +2 -2
  3. package/src/cli/args.js +65 -29
  4. package/src/cli/errors.js +9 -7
  5. package/src/cli/help.js +51 -3
  6. package/src/commands/doctor.js +80 -78
  7. package/src/commands/full.js +11 -2
  8. package/src/commands/interactive.js +71 -49
  9. package/src/commands/issues.js +4 -0
  10. package/src/commands/skills.js +8 -7
  11. package/src/commands/version.js +4 -2
  12. package/src/commands/workflows.js +3 -1
  13. package/src/context.js +4 -0
  14. package/src/core/breaking-check.js +6 -5
  15. package/src/core/copy/simple.js +4 -0
  16. package/src/core/copy/workflows.js +10 -5
  17. package/src/core/exclusions.js +8 -0
  18. package/src/core/ide/adapters/claude.js +18 -17
  19. package/src/core/ide/adapters/codex.js +12 -11
  20. package/src/core/ide/adapters/cursor.js +11 -10
  21. package/src/core/ide/adapters/gemini.js +11 -10
  22. package/src/core/ide/adapters/pi-common.js +2 -1
  23. package/src/core/ide/adapters/pi-harness.js +13 -16
  24. package/src/core/ide/adapters/pi.js +12 -11
  25. package/src/core/label-style.js +78 -0
  26. package/src/core/migration-guide.js +38 -38
  27. package/src/core/migrations/index.js +19 -15
  28. package/src/core/migrations/registry.js +70 -55
  29. package/src/core/options-ask.js +65 -64
  30. package/src/core/orphan-workflows.js +2 -1
  31. package/src/core/paths-resolve.js +31 -30
  32. package/src/core/repo-language.js +47 -0
  33. package/src/core/run-trace.js +10 -0
  34. package/src/core/version-yml.js +68 -36
  35. package/src/i18n/core.en.js +31 -0
  36. package/src/i18n/core.ko.js +31 -0
  37. package/src/i18n/doctor.en.js +89 -0
  38. package/src/i18n/doctor.ko.js +89 -0
  39. package/src/i18n/en.js +141 -0
  40. package/src/i18n/ide.en.js +102 -0
  41. package/src/i18n/ide.ko.js +102 -0
  42. package/src/i18n/index.js +73 -0
  43. package/src/i18n/ko.js +134 -0
  44. package/src/i18n/migrate.en.js +66 -0
  45. package/src/i18n/migrate.ko.js +66 -0
  46. package/src/i18n/options.en.js +104 -0
  47. package/src/i18n/options.ko.js +104 -0
  48. package/src/i18n/summary.en.js +70 -0
  49. package/src/i18n/summary.ko.js +70 -0
  50. package/src/index.js +46 -28
  51. package/src/ui/banner.js +8 -6
  52. package/src/ui/env-plan.js +19 -18
  53. package/src/ui/prompts.js +25 -24
  54. package/src/ui/readline-engine.js +18 -10
  55. package/src/ui/skills-prompts.js +8 -7
  56. package/src/ui/status-cards.js +38 -38
  57. package/src/ui/summary.js +103 -111
@@ -16,6 +16,8 @@ import {
16
16
  import { copyUtilModules } from "../core/copy/util.js";
17
17
  import { copyCoderabbit } from "../core/copy/coderabbit.js";
18
18
  import { ensureGitignore } from "../core/copy/gitignore.js";
19
+ import { applyLabelStyle } from "../core/label-style.js";
20
+ import { applyRepoLanguage } from "../core/repo-language.js";
19
21
  import { verifyInstall } from "../core/verify.js";
20
22
 
21
23
  // context: { version, types, paths:Map, branch, versionCode, deployTarget, publishTargets, includeSecretBackup,
@@ -26,7 +28,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
26
28
  force = true, now, today, templateVersion = "unknown",
27
29
  deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true,
28
30
  changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
29
- deployBranch = "", intent = null, semverAuto = true , appRelease = null } = context;
31
+ deployBranch = "", intent = null, semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = "en" } = context;
30
32
 
31
33
  // project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
32
34
  const pathMarkers = new Map();
@@ -48,7 +50,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
48
50
  version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
49
51
  deployValues,
50
52
  templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, aiPrSummary, optionsDate: today,
51
- changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease },
53
+ changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language },
52
54
  })), { version, versionCode });
53
55
 
54
56
  // 2. README 버전 섹션
@@ -66,6 +68,10 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
66
68
  // 7. issue / discussion 템플릿
67
69
  step("copy-templates", () => {
68
70
  copyIssueTemplates(tempDir, targetRoot);
71
+ // 템플릿 언어(#769) — 원본은 영문이라 한국어 레포는 복사 직후 오버레이로 덮는다.
72
+ // 라벨 표기 변환(apply-label-style)보다 반드시 먼저여야 한다.
73
+ const overlaid = applyRepoLanguage(tempDir, targetRoot, language);
74
+ hooks.trace?.event("copy", "repo-language", language, { overlaid: overlaid.length });
69
75
  copyDiscussionTemplates(tempDir, targetRoot);
70
76
  });
71
77
 
@@ -76,6 +82,9 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
76
82
  step("ensure-gitignore", () => ensureGitignore(targetRoot));
77
83
  step("copy-setup-guide", () => copySetupGuide(tempDir, targetRoot));
78
84
 
85
+ // 상태 라벨 표기(#776) — 템플릿 원본은 영문이라, 한글 레포는 복사가 끝난 뒤 이름만 되돌린다.
86
+ step("apply-label-style", () => applyLabelStyle(targetRoot, labelStyle), { labelStyle });
87
+
79
88
  // 9. 설치 후 검증 (#549) — 디스크에 쓰인 최종 결과물을 다시 읽는다.
80
89
  // 치환은 파일 단위로 흩어져 일어나고 auto 토큰은 resolver 결과에 의존하므로,
81
90
  // 최종 내용을 보는 것이 실제 배포될 것과 같은 것을 보는 유일한 방법이다.
@@ -26,6 +26,10 @@ import { runWorkflows } from "./workflows.js";
26
26
  import { runIssues } from "./issues.js";
27
27
  import { runSkills } from "./skills.js";
28
28
  import * as prompts from "../ui/prompts.js";
29
+ import { t, getLang, withLang } from "../i18n/index.js";
30
+ import { resolveLabelStyle } from "../core/label-style.js";
31
+ import { resolveRepoLanguage } from "../core/repo-language.js";
32
+ import { printSummary } from "../ui/summary.js";
29
33
 
30
34
  const CANCEL = prompts.CANCEL;
31
35
  const isCancel = (v) => v === CANCEL || typeof v === "symbol";
@@ -54,8 +58,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
54
58
  traceTo = templateVersion;
55
59
 
56
60
  // 층1 — 시작 배너 (#446 확정 시안 A). 스텁엔 banner 없음 → intro 폴백.
57
- if (io.banner) io.banner({ version: templateVersion, modeLabel: "대화형 통합 마법사" });
58
- else io.intro?.("projectops — 대화형 통합 마법사");
61
+ if (io.banner) io.banner({ version: templateVersion, modeLabel: t("banner.interactive") });
62
+ else io.intro?.(`projectops, ${t("banner.interactive")}`);
59
63
 
60
64
  // 기존 version.yml — version/version_code/paths/옵션 보존의 단일 진실 (.sh SSoT L2208~2239)
61
65
  const vyPath = join(cwd, "version.yml");
@@ -63,7 +67,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
63
67
  traceFrom = existing?.templateVersion || "";
64
68
 
65
69
  // 층4 — IDE Skills 현재 상태 · 층5 — 신규/업데이트 판별 (#446)
66
- io.ideStatus?.();
67
70
  io.installKind?.({ currentTemplateVersion: existing?.templateVersion || "", templateVersion });
68
71
 
69
72
  // 1) 모드 선택 — 기존 통합 레포면 업데이트 항목을 맨 위에 노출 (#502)
@@ -72,7 +75,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
72
75
  trace.event("run", "start", "interactive", { templateVersion, isUpdate: !!updateInfo });
73
76
  const picked = await io.selectMode(updateInfo ? { update: updateInfo } : {});
74
77
  trace.event("prompt", "mode", String(picked ?? ""), { update: !!updateInfo });
75
- if (picked === CANCEL || picked == null) { trace.event("run", "cancelled", "mode-select", { reason: "user-cancel" }); io.cancelMessage?.("설치를 취소했습니다."); return 0; }
78
+ if (picked === CANCEL || picked == null) { trace.event("run", "cancelled", "mode-select", { reason: "user-cancel" }); io.cancelMessage?.(t("flow.cancelInstall")); return 0; }
76
79
  // 업데이트 모드(#502): 저장된 통합 범위(templateMode, 없으면 full)를 재실행하고
77
80
  // 이하 updateRun 분기가 질문을 최소화한다 ("저장된 설정 그대로 반영"이 계약).
78
81
  const updateRun = picked === "update";
@@ -85,12 +88,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
85
88
  askYesNo: (msg, def) => io.askYesNo(msg, def),
86
89
  onItems: (items) => { breakingReport = items; },
87
90
  }));
88
- if (!proceed) { trace.event("run", "cancelled", "breaking-gate", { reason: "user-declined" }); io.cancelMessage?.("통합을 안전하게 취소했습니다."); return 0; }
91
+ if (!proceed) { trace.event("run", "cancelled", "breaking-gate", { reason: "user-declined" }); io.cancelMessage?.(t("flow.cancelBreaking")); return 0; }
89
92
 
90
93
  // skills 모드 — IDE 스킬 설치 (템플릿 통합 없음). 대화형으로 실행.
91
94
  if (mode === "skills") {
95
+ io.ideStatus?.();
92
96
  await skills({ templateVersion, tempDir, interactive: true });
93
- io.outro?.("AI 스킬 설치를 마쳤습니다.");
97
+ io.outro?.(t("flow.doneSkills"));
94
98
  return 0;
95
99
  }
96
100
 
@@ -99,7 +103,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
99
103
  const ctx = createContext({ ...baseCtx, mode, force: true });
100
104
  runIssues(ctx, tempDir, cwd);
101
105
  io.summary?.({ mode, types: [], version: "", counters: {} }, cwd);
102
- io.outro?.("이슈·PR 템플릿을 설치했습니다.");
106
+ io.outro?.(t("flow.doneIssues"));
103
107
  return 0;
104
108
  }
105
109
 
@@ -130,8 +134,26 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
130
134
  // 저장값이 있으면 보존, 없으면 신규 통합만 ON. 기존 레포는 업데이트만으로 버전이 튀지 않는다.
131
135
  const semverAuto = existing?.options?.semverAuto ?? (existing ? false : true);
132
136
  const appRelease = existing?.options?.appRelease ?? null; // #553 저장값 보존 (묻지 않음)
137
+ // 상태 라벨 표기(#776): 저장값 > (신규 en / 기존 ko). 기존 한글 레포에는 영문 전환을 한 번 제안한다(아래).
138
+ const closeOnRelease = existing?.options?.closeOnRelease ?? (existing ? null : true); // #771 묻지 않는다
139
+ // Projects 보드 동기화(#716): 묻지 않는다. 저장값 > 이미 설치돼 있으면 유지 > 신규는 제외 (index.js 와 같은 규칙).
140
+ const projectsSync = existing?.options?.projectsSync
141
+ ?? existsSync(join(cwd, ".github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml"));
142
+ // 레포 문구 언어(#769): 저장값 > (신규 en / 기존 ko). 기존 레포에는 영문 전환을 한 번만 제안한다.
143
+ let language = resolveRepoLanguage({ flag: null, stored: existing?.options?.language, existing: !!existing });
144
+ let labelStyle = resolveLabelStyle({ flag: null, stored: existing?.options?.labelStyle, existing: !!existing });
133
145
  const showOptional = mode === "full" || mode === "workflows";
134
146
  const realTty = process.stdout.isTTY === true;
147
+ // 기존 한글 라벨 레포에는 영문 표준 전환을 한 번만 제안한다(#776). 거절하면 한글을 저장해 다시 묻지 않는다.
148
+ // 라벨 정의의 from_name 이 이슈에 붙은 라벨을 그대로 옮기므로 이슈 이력은 보존된다.
149
+ if (existing && !existing.options?.labelStyle && realTty && showOptional && io.askYesNo) {
150
+ const adopt = await io.askYesNo(t("labelStyle.propose"), false);
151
+ labelStyle = adopt ? "en" : "ko";
152
+ }
153
+ if (existing && !existing.options?.language && realTty && showOptional && io.askYesNo) {
154
+ const adopt = await io.askYesNo(t("language.propose"), false);
155
+ language = adopt ? "en" : "ko";
156
+ }
135
157
 
136
158
  // 층2 — 감지 로그 (#446)
137
159
  io.detectionLog?.({ types, version, branch });
@@ -174,9 +196,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
174
196
  // 업데이트 모드 — 카드는 보여주되 확인 루프 생략 (#502: 설정 변경은 기존 모드의 몫)
175
197
  if (updateRun) {
176
198
  if (io.analysisCard) {
177
- io.analysisCard({ mode, modeLabel: `업데이트 (${modeLabel(mode)} 범위)`, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, paths });
199
+ io.analysisCard({ mode, modeLabel: t("flow.updateScope", { scope: modeLabel(mode) }), types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, paths });
178
200
  } else {
179
- io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), "업데이트 — 저장된 설정으로 진행");
201
+ io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), t("flow.title.update"));
180
202
  }
181
203
  confirmed = true;
182
204
  }
@@ -185,10 +207,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
185
207
  if (io.analysisCard) {
186
208
  io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, paths });
187
209
  } else {
188
- io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), "프로젝트 분석 결과");
210
+ io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), t("card.title"));
189
211
  }
190
212
  const choice = await io.confirmProjectMenu();
191
- if (choice === "cancel") { trace.event("run", "cancelled", "confirm-loop", { reason: "user-cancel" }); io.cancelMessage?.("설치를 취소했습니다."); return 0; }
213
+ if (choice === "cancel") { trace.event("run", "cancelled", "confirm-loop", { reason: "user-cancel" }); io.cancelMessage?.(t("flow.cancelInstall")); return 0; }
192
214
  if (isCancel(choice) || choice == null) continue; // ESC = 머무르기 (루프 재출력)
193
215
  if (choice === "continue") { confirmed = true; break; }
194
216
  // edit 루프
@@ -207,14 +229,14 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
207
229
  if ([...types].sort().join(",") !== oldSorted) paths = new Map();
208
230
  }
209
231
  } else if (what === "version") {
210
- const v = await io.askText("새 버전 (예: 1.0.0)", version);
232
+ const v = await io.askText(t("flow.askVersion"), version);
211
233
  if (!isCancel(v) && v !== version) {
212
234
  // semver 형식 검증 (.sh L2010~2015)
213
235
  if (/^\d+\.\d+\.\d+$/.test(v)) version = v;
214
- else io.note?.("버전 형식이 올바르지 않습니다 (x.y.z 형태) — 기존 값을 유지합니다.", "⚠ 버전");
236
+ else io.note?.(t("flow.badVersion"), t("flow.badVersionTitle"));
215
237
  }
216
238
  } else if (what === "branch") {
217
- const b = await io.askText("기본 브랜치", branch);
239
+ const b = await io.askText(t("flow.askBranch"), branch);
218
240
  if (!isCancel(b) && b) branch = b;
219
241
  } else if (OPTION_AXES.includes(what)) {
220
242
  // #483 — 선택한 축 하나만 재질문 (scope). 나머지 옵션은 현재값 그대로 유지.
@@ -279,8 +301,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
279
301
  const liteCtx = { types, paths, deployTarget, publishTargets, repoName, resolvers, branch, deployBranch };
280
302
  const conflicts = listWorkflowConflicts(liteCtx, tempDir, cwd);
281
303
  if (conflicts.length) {
282
- io.note?.(conflicts.map((c) => `• ${c.filename}`).join("\n"), `♻️ 템플릿이 갱신된 워크플로우 ${conflicts.length}개`);
283
- const yes = await io.askYesNo(`위 ${conflicts.length}개를 .bak 백업 후 새 버전으로 교체할까요? (기존 설정값은 유지됩니다)`, true);
304
+ io.note?.(conflicts.map((c) => `• ${c.filename}`).join("\n"), t("flow.conflictsTitle", { count: conflicts.length }));
305
+ const yes = await io.askYesNo(t("flow.conflictsAsk", { count: conflicts.length }), true);
284
306
  trace.event("prompt", "conflict-bulk", yes ? "backup" : "skip", { count: conflicts.length, files: conflicts.map((c) => c.filename) });
285
307
  const decision = yes === true ? "backup" : "skip";
286
308
  updateDecisions = new Map();
@@ -300,7 +322,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
300
322
  const { now, today } = clock || utcNow();
301
323
  const ctx = createContext({
302
324
  mode, force: true, types, version, versionCode, branch, paths, deployTarget, publishTargets, includeSecretBackup,
303
- codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent, semverAuto, appRelease,
325
+ codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language,
304
326
  repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
305
327
  // #502 — version 모드가 기존 full 기록을 강등하지 않도록 (full이 우세)
306
328
  recordMode: existing?.templateMode === "full" ? "full" : "version",
@@ -318,11 +340,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
318
340
  for (const { filename, type } of conflicts) {
319
341
  if (!perType.has(type)) {
320
342
  const sel = await io.engineIo.select({
321
- message: `기존 워크플로우와 내용이 다른 파일이 있습니다 (${type}) — 어떻게 할까요?`,
343
+ message: t("flow.conflictType", { type }),
322
344
  options: [
323
- { value: "skip", label: "건너뛰기 — 기존 파일 유지 (기본)" },
324
- { value: "backup", label: ".bak 백업 후 새 버전으로 교체" },
325
- { value: "template", label: "기존 유지 + 새 버전을 .template.yaml로 참고 추가" },
345
+ { value: "skip", label: t("flow.opt.skip") },
346
+ { value: "backup", label: t("flow.opt.backup") },
347
+ { value: "template", label: t("flow.opt.template") },
326
348
  ],
327
349
  });
328
350
  perType.set(type, isCancel(sel) || sel == null ? "skip" : sel); // ESC = 건너뛰기 (.sh L3463)
@@ -349,13 +371,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
349
371
  if (mode === "full" || mode === "workflows") {
350
372
  const orphans = detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types,
351
373
  // 껐는데 남아 계속 도는 워크플로우도 함께 잡는다 (#566)
352
- options: { includeSecretBackup, aiPrSummary: aiPrSummary !== false, deployTarget } });
374
+ options: { includeSecretBackup, aiPrSummary: aiPrSummary !== false, projectsSync, deployTarget } });
353
375
  if (orphans.length > 0) {
354
376
  io.note?.(
355
- orphans.map((o) => `• ${o.filename} (${o.type} 타입 — 현재 미선택)`).join("\n"),
356
- `🧹 선택되지 않은 타입의 워크플로우 ${orphans.length}개 발견`,
377
+ orphans.map((o) => `• ${t("flow.orphanLine", { file: o.filename, type: o.type })}`).join("\n"),
378
+ t("flow.orphanTitle", { count: orphans.length }),
357
379
  );
358
- const yes = await io.askYesNo(`위 ${orphans.length}개를 정리할까요? (.bak 무해화 — 복원 가능)`, true);
380
+ const yes = await io.askYesNo(t("flow.orphanAsk", { count: orphans.length }), true);
359
381
  trace.event("prompt", "orphan-cleanup", yes ? "clean" : "keep", { count: orphans.length });
360
382
  if (yes === true) {
361
383
  const results = applyOrphanCleanup(cwd, orphans);
@@ -363,7 +385,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
363
385
  const failed = results.filter((r) => r.action === "error");
364
386
  orphanReport.cleaned = ok.map((r) => r.filename);
365
387
  for (const r of ok) trace.event("orphan", "neutralized", r.filename);
366
- io.note?.(`✅ 고아 워크플로우 정리: ${ok.length}개${failed.length ? ` (실패 ${failed.length}개)` : ""}`, "정리 완료");
388
+ io.note?.(t("flow.orphanDone", { ok: ok.length, failed: failed.length ? t("flow.orphanFailed", { count: failed.length }) : "" }), t("flow.orphanDoneTitle"));
367
389
  } else {
368
390
  orphanReport.pending = orphans.map((o) => o.filename);
369
391
  }
@@ -380,8 +402,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
380
402
  if (updateRun) {
381
403
  await skills({ templateVersion, tempDir, interactive: false, installedOnly: true });
382
404
  } else {
383
- const wantSkills = await io.askYesNo("AI 에이전트 스킬(Claude·Cursor·Gemini·Codex·PI)도 설치/업데이트할까요?", false);
384
- if (wantSkills === true) await skills({ templateVersion, tempDir, interactive: true });
405
+ const wantSkills = await io.askYesNo(t("flow.askSkills"), false);
406
+ if (wantSkills === true) { io.ideStatus?.(); await skills({ templateVersion, tempDir, interactive: true }); }
385
407
  }
386
408
 
387
409
  // 마이그레이션 기록 (#493/#494) — Layer 2/3 트레이스 파일 + Layer 1 가이드 엔트리 (full/workflows만)
@@ -395,7 +417,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
395
417
  migrationGuidePath = appendGuideEntry(cwd, {
396
418
  now, mode, types, repoName,
397
419
  templateFrom: existing?.templateVersion || "", templateTo: templateVersion,
398
- options: { deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup, coderabbit: codeReviewCoderabbit, changelogProvider, intent, semverAuto , appRelease },
420
+ options: { deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup, coderabbit: codeReviewCoderabbit, changelogProvider, intent, semverAuto , appRelease, labelStyle, closeOnRelease, projectsSync, language },
399
421
  branches: { defaultBranch: branch, deployBranch, ready: deployBranchReady, created: deployBranchCreated },
400
422
  breaking: breakingReport, migrations: migrationsResult, orphans: orphanReport,
401
423
  events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
@@ -404,7 +426,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
404
426
  }
405
427
 
406
428
  // 완료 요약 (.sh print_summary L5438)
407
- io.summary?.({
429
+ const summaryCtx = {
408
430
  mode, types, version, deployBranch, deployBranchReady, migrationGuidePath,
409
431
  counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
410
432
  skippedConflicts: result?.workflows?.skippedConflicts ?? [], // #654 병합 안내
@@ -414,8 +436,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
414
436
  logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
415
437
  logFile: files?.logFile ?? null,
416
438
  traceFile: files?.traceFile ?? null,
417
- }, cwd);
418
- io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
439
+ };
440
+ io.summary?.(summaryCtx, cwd);
441
+ // The run log is English only: when the screen is in another language, add an English copy of this screen.
442
+ if (getLang() !== "en") {
443
+ const buf = [];
444
+ withLang("en", () => printSummary(summaryCtx, cwd, (x) => buf.push(x)));
445
+ trace.appendText(buf.join(""));
446
+ }
447
+ io.outro?.(t("flow.done", { mode }));
419
448
 
420
449
  // 완료 화면까지 출력한 뒤 finally에서 닫는다 (#561) — 여기서 닫으면 이 아래 화면이 안 담긴다.
421
450
  trace.event("run", "end", mode || "", {
@@ -442,30 +471,23 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
442
471
 
443
472
  function summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }) {
444
473
  const lines = [
445
- `통합 모드 : ${modeLabel(mode)}`,
446
- `프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
447
- `버전 : ${version}`,
448
- `기본 브랜치 : ${branch}`,
474
+ `${t("sum.mode")}: ${modeLabel(mode)}`,
475
+ `${t("sum.types")}: ${types.join(", ")}${types.length > 1 ? t("sum.multi") : ""}`,
476
+ `${t("card.version")}: ${version}`,
477
+ `${t("edit.branch")}: ${branch}`,
449
478
  ];
450
479
  if (showOptional) {
451
- lines.push(`배포 방식 : ${deployTarget || "docker-ssh"}`);
452
- lines.push(`Publish : ${(publishTargets ?? []).join(",") || "없음"}`);
453
- lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
454
- lines.push(`Changelog : ${changelogProvider || "commit"}`);
455
- lines.push(`CodeRabbit 리뷰 : ${codeReviewCoderabbit ? "사용" : "미사용"}`);
480
+ lines.push(`${t("sum.deploy")}: ${deployTarget || "docker-ssh"}`);
481
+ lines.push(`${t("sum.publish")}: ${(publishTargets ?? []).join(",") || t("card.none")}`);
482
+ lines.push(`${t("sum.secret")}: ${includeSecretBackup ? t("card.included") : t("card.excluded")}`);
483
+ lines.push(`${t("sum.changelog")}: ${changelogProvider || "commit"}`);
484
+ lines.push(`${t("sum.coderabbit")}: ${codeReviewCoderabbit ? t("sum.on") : t("sum.off")}`);
456
485
  }
457
486
  return lines.join("\n");
458
487
  }
459
488
 
460
489
  function modeLabel(m) {
461
- return {
462
- full: "전체 설치 (버전관리 + 워크플로우 + 템플릿)",
463
- version: "버전 관리 전용 (자동화 시스템)",
464
- workflows: "워크플로우 전용 (GitHub Actions 빌드, 배포)",
465
- issues: "이슈/PR 템플릿 전용",
466
- skills: "AI 스킬 전용 (Claude, Cursor, Gemini, Codex, PI)",
467
- update: "업데이트"
468
- }[m] || m;
490
+ return m === "update" ? t("mode.update.short") : (t(`mode.${m}`) === `mode.${m}` ? m : t(`mode.${m}`));
469
491
  }
470
492
 
471
493
  function utcNow(date = new Date()) {
@@ -1,8 +1,12 @@
1
1
  // issues 모드 (.sh execute_integration issues case 등가) — template_integrator.sh 4532~4535.
2
2
  // 이슈/PR 템플릿 + Discussion 템플릿만 복사.
3
+ import { applyLabelStyle } from "../core/label-style.js";
4
+ import { applyRepoLanguage } from "../core/repo-language.js";
3
5
  import { copyIssueTemplates, copyDiscussionTemplates } from "../core/copy/simple.js";
4
6
 
5
7
  export function runIssues(context, tempDir, targetRoot = ".") {
6
8
  copyIssueTemplates(tempDir, targetRoot);
9
+ applyRepoLanguage(tempDir, targetRoot, context.language ?? "en"); // #769 — 라벨 변환보다 먼저
10
+ applyLabelStyle(targetRoot, context.labelStyle ?? "en"); // #776
7
11
  copyDiscussionTemplates(tempDir, targetRoot);
8
12
  }
@@ -5,6 +5,7 @@ import { ADAPTERS } from "../core/ide/registry.js";
5
5
  import { versionTag } from "../core/ide/util.js";
6
6
  import { defaultIo } from "../core/ide/runner.js";
7
7
  import * as skillsPrompts from "../ui/skills-prompts.js";
8
+ import { t } from "../i18n/index.js";
8
9
 
9
10
  // 상태 수집 → [{adapter, status}]. optional 어댑터는 감지 불가(cliMissing)면 목록에서 제외.
10
11
  export function collectStatuses(io) {
@@ -21,12 +22,12 @@ export function collectStatuses(io) {
21
22
  export function formatStatuses(rows, templateVersion) {
22
23
  return rows.map(({ adapter, status }) => {
23
24
  const label = adapter.label.padEnd(12);
24
- if (status.cliMissing) return `${label}: ${status.note || "skill 미설치 (CLI 없음)"}`;
25
+ if (status.cliMissing) return `${label}: ${status.note || t("skills.status.noCli")}`;
25
26
  if (status.installed) {
26
27
  const v = status.version ? ` (v${status.version})` : "";
27
- return `${label}: skill 설치됨${v}${versionTag(status.version, templateVersion)}`;
28
+ return `${label}: ${t("skills.status.installed")}${v}${versionTag(status.version, templateVersion)}`;
28
29
  }
29
- return `${label}: ${status.note || "skill 미설치"}`;
30
+ return `${label}: ${status.note || t("skills.status.notInstalled")}`;
30
31
  });
31
32
  }
32
33
 
@@ -40,7 +41,7 @@ export async function runSkills(opts = {}) {
40
41
 
41
42
  const rows = collectStatuses(io);
42
43
  io.log("");
43
- io.log("── IDE Skills 현재 상태 ──");
44
+ io.log(t("skills.statusTitle"));
44
45
  for (const line of formatStatuses(rows, ctx.templateVersion)) io.log(line);
45
46
  io.log("");
46
47
 
@@ -57,7 +58,7 @@ export async function runSkills(opts = {}) {
57
58
  // 대화형: 동작 선택 → IDE 멀티셀렉트 → 실행.
58
59
  const action = await ui.selectAction();
59
60
  if (action == null || action === skillsPrompts.CANCEL || action === "skip") {
60
- io.log("IDE Skills는 변경하지 않고 넘어갑니다.");
61
+ io.log(t("skills.unchanged"));
61
62
  return 0;
62
63
  }
63
64
 
@@ -71,7 +72,7 @@ export async function runSkills(opts = {}) {
71
72
 
72
73
  const targets = await ui.selectTargets(choices, preselect, action);
73
74
  if (targets == null || targets === skillsPrompts.CANCEL || !targets.length) {
74
- io.log("선택한 IDE가 없어 건너뜁니다 (원할 때 다시 실행하세요).");
75
+ io.log(t("skills.noneSelected"));
75
76
  return 0;
76
77
  }
77
78
 
@@ -79,7 +80,7 @@ export async function runSkills(opts = {}) {
79
80
  for (const { adapter } of rows) {
80
81
  if (!selected.has(adapter.id)) continue;
81
82
  io.log("");
82
- io.log(`[ ${adapter.label} ${action === "apply" ? "설치/업데이트" : "제거"} ]`);
83
+ io.log(t(action === "apply" ? "skills.header.apply" : "skills.header.remove", { label: adapter.label }));
83
84
  safe(() => (action === "apply" ? adapter.apply(io, ctx) : adapter.remove(io, ctx)), false);
84
85
  }
85
86
  return 0;
@@ -8,6 +8,7 @@ import { PATHS } from "../core/paths.js";
8
8
  import { buildVersionYml, mergeDeployValues } from "../core/version-yml.js";
9
9
  import { markerForType } from "../core/detect.js";
10
10
  import { addVersionSectionToReadme } from "../core/copy/readme.js";
11
+ import { applyLabelStyle } from "../core/label-style.js";
11
12
  import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
12
13
  import { ensureGitignore } from "../core/copy/gitignore.js";
13
14
 
@@ -15,7 +16,7 @@ export function runVersion(context, tempDir, targetRoot = ".") {
15
16
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
16
17
  now, today, templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false,
17
18
  changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
18
- deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null } = context;
19
+ deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = null } = context;
19
20
 
20
21
  const pathMarkers = new Map();
21
22
  for (const [t] of paths) pathMarkers.set(t, markerForType(t));
@@ -31,11 +32,12 @@ export function runVersion(context, tempDir, targetRoot = ".") {
31
32
  // mode(#502): version 모드가 기존 full 통합 기록을 "version"으로 강등하지 않도록
32
33
  // 호출부가 recordMode로 기존 값을 넘긴다 (full이 우세 — 업데이트 재실행 범위 축소 방지).
33
34
  templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate: today,
34
- changelogProvider, changelogBaseUrl, codeReviewCoderabbit, mode: recordMode, semverAuto, appRelease },
35
+ changelogProvider, changelogBaseUrl, codeReviewCoderabbit, mode: recordMode, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language },
35
36
  }));
36
37
  addVersionSectionToReadme(version, targetRoot);
37
38
  copyScripts(tempDir, targetRoot);
38
39
  copyConfigFolder(tempDir, targetRoot);
40
+ applyLabelStyle(targetRoot, labelStyle); // #776
39
41
  ensureGitignore(targetRoot);
40
42
  copySetupGuide(tempDir, targetRoot);
41
43
  }
@@ -5,13 +5,14 @@ import { join } from "node:path";
5
5
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { PATHS } from "../core/paths.js";
7
7
  import { copyWorkflows } from "../core/copy/workflows.js";
8
+ import { applyLabelStyle } from "../core/label-style.js";
8
9
  import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
9
10
  import { copyUtilModules } from "../core/copy/util.js";
10
11
  import { convertLegacySingularType, mergeDeployValues } from "../core/version-yml.js";
11
12
  import { verifyInstall } from "../core/verify.js";
12
13
 
13
14
  export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
14
- const { types = [], force = true } = context;
15
+ const { types = [], force = true, labelStyle = "en" } = context;
15
16
  const wf = copyWorkflows(context, tempDir, targetRoot, hooks);
16
17
 
17
18
  const vy = join(targetRoot, PATHS.versionFile);
@@ -31,6 +32,7 @@ export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
31
32
 
32
33
  copyScripts(tempDir, targetRoot);
33
34
  copyConfigFolder(tempDir, targetRoot);
35
+ applyLabelStyle(targetRoot, labelStyle); // #776 — 워크플로우(QA 봇)와 라벨 정의를 같은 표기로 맞춘다
34
36
  for (const t of types) copyUtilModules(tempDir, t, { force }, targetRoot);
35
37
  copySetupGuide(tempDir, targetRoot);
36
38
 
package/src/context.js CHANGED
@@ -19,6 +19,7 @@ export function createContext(overrides = {}) {
19
19
  deployTarget: null, // 'docker-ssh'(기본) | 'vercel' | 'none'
20
20
  publishTargets: null, // ['nexus','npm','github-packages'] 부분집합
21
21
  includeSecretBackup: null,
22
+ projectsSync: null, // Projects 보드 동기화 포함 여부 (#716). null=미설정(신규 false / 이미 설치된 레포는 유지)
22
23
  aiPrSummary: null, // #566 — AI 변경 요약 워크플로우 포함 여부
23
24
  // changelog provider 축 (#455 — null=미설정)
24
25
  changelogProvider: null, // 'commit'(기본) | 'coderabbit' | 'openai' | 'gemini' | 'claude' | 'ollama' | 'commit'
@@ -27,6 +28,9 @@ export function createContext(overrides = {}) {
27
28
  deployBranch: "", // 릴리스 PR head 브랜치 (#456). 빈 값=metadata.deploy_branch 미출력
28
29
  intent: null, // 프로젝트 성격 (#485 — app/library/both/none/manual). null=미설정(역추론)
29
30
  semverAuto: null, // semver 자동 승격 (#546). null=미설정(신규 통합 true / 기존 레포 false)
31
+ closeOnRelease: null, // 릴리스 시 완료 이슈 닫기 (#771). null=미설정(신규 true / 기존 레포 키 없음)
32
+ language: null, // 레포 문구 언어 (#769). null=미설정(신규 en / 기존 레포 ko)
33
+ labelStyle: null, // 상태 라벨 표기 (#776). null=미설정(신규 en / 기존 레포 ko)
30
34
  appRelease: null, // 앱 심사 배포 레포인가 (#553). null=미설정(키 기록 안 함)
31
35
  templateVersion: "",
32
36
  tempDir: "",
@@ -4,6 +4,7 @@ import { readFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { collectBreaking } from "./breaking.js";
6
6
  import { parseExisting } from "./version-yml.js";
7
+ import { t } from "../i18n/index.js";
7
8
 
8
9
  const BC_URL = "https://raw.githubusercontent.com/Cassiiopeia/projectops/main/.github/config/breaking-changes.json";
9
10
 
@@ -46,7 +47,7 @@ export async function runBreakingCheck({ cwd, tempDir, templateVersion, askYesNo
46
47
  // 긴 본문 래핑이 ║ 박스 경계를 붕괴시켰다. 버전·제목만 한 줄씩, 전문은 선택 열람.)
47
48
  const e = (s = "") => process.stderr.write(s + "\n");
48
49
  e("");
49
- e(`⚠️ BREAKING CHANGES (v${current} -> v${templateVersion}) - CRITICAL ${critical.length}건, WARNING ${warnings.length}건`);
50
+ e(t("breaking.title", { current, target: templateVersion, critical: critical.length, warnings: warnings.length }));
50
51
  e("");
51
52
  for (const c of critical) e(` ❗ [CRITICAL] ${c.version} - ${c.title || ""}`);
52
53
  for (const w of warnings) e(` ⚠️ [WARNING] ${w.version} - ${w.title || ""}`);
@@ -54,7 +55,7 @@ export async function runBreakingCheck({ cwd, tempDir, templateVersion, askYesNo
54
55
 
55
56
  if (askYesNo) {
56
57
  // 대화형: 전문(조치 방법)은 원할 때만 펼친다
57
- const detail = await askYesNo("각 항목의 상세 내용(조치 방법)을 볼까요?", false);
58
+ const detail = await askYesNo(t("breaking.askDetail"), false);
58
59
  if (detail === true) {
59
60
  for (const it of [...critical, ...warnings]) {
60
61
  e("");
@@ -64,7 +65,7 @@ export async function runBreakingCheck({ cwd, tempDir, templateVersion, askYesNo
64
65
  e("");
65
66
  }
66
67
  } else {
67
- e(" 상세 내용, 조치 방법: .github/config/breaking-changes.json 참고");
68
+ e(t("breaking.seeJson"));
68
69
  e(` (${BC_URL})`);
69
70
  e("");
70
71
  }
@@ -72,11 +73,11 @@ export async function runBreakingCheck({ cwd, tempDir, templateVersion, askYesNo
72
73
  if (critical.length > 0) {
73
74
  if (askYesNo) {
74
75
  // 대화형: 명시 확인 없으면 중단 (기본 N — .sh 등가)
75
- const ok = await askYesNo("위 호환성 변경을 확인했고 계속 진행할까요?", false);
76
+ const ok = await askYesNo(t("breaking.askContinue"), false);
76
77
  if (ok !== true) return false;
77
78
  } else {
78
79
  // 비대화형(--force): 게이트로 CI를 죽이지 않고 경고 후 진행 (.sh와 의도적 차이 — CI 친화)
79
- e("⚠️ CRITICAL 호환성 변경이 있습니다 — 비대화형 실행이라 계속 진행합니다. 위 내용을 꼭 확인하세요.");
80
+ e(t("breaking.nonInteractive"));
80
81
  }
81
82
  }
82
83
  return true;
@@ -31,6 +31,10 @@ export function copyScripts(tempDir, targetRoot = ".") {
31
31
  "asc_client.py",
32
32
  // iOS 버전 사전 점검, 업로드 거부 분류, 아카이브부터 업로드까지 재시도 (#643).
33
33
  "ios_release.py",
34
+ // 릴리스 머지 때 완료 라벨이 붙은 이슈를 닫는다 (#771). close_on_release 옵션이 켜진 레포에서만 동작한다.
35
+ "close_issues_on_release.py",
36
+ // Play Store/TestFlight 배포 모드를 코드 설정(.github/config/store-deploy.json)에서 읽는다 (#767).
37
+ "store_deploy_config.py",
34
38
  "changelog_providers/_common.py", "changelog_providers/ladder.py",
35
39
  "changelog_providers/commit.py", "changelog_providers/copilot.py",
36
40
  "changelog_providers/openai_compatible.py",
@@ -71,7 +71,7 @@ function classify(srcDir, workflowsDir, envOpts, baseline = null) {
71
71
  // hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'> } — 기존 파일(changed) 충돌 결정.
72
72
  // 반환: {copied, skipped, templateAdded, optionalCopied, copiedFiles[]} — copiedFiles는 실제 복사·교체된 파일명 (#473 요약용)
73
73
  export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
74
- const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
74
+ const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true, projectsSync = true, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
75
75
  const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
76
76
  const trace = hooks.trace ?? null; // #494 — 실행 트레이스 (null-safe: 미주입이면 전 이벤트 no-op)
77
77
  const workflowsDir = join(targetRoot, PATHS.workflowsDir);
@@ -193,14 +193,18 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
193
193
  // (4.7) common/pr-summary — AI 변경 요약 (#566). 선택했을 때만 복사한다.
194
194
  // 종전에는 common 본체에 있어 무조건 복사된 뒤 런타임에 스스로 빠졌고, 그 판단이
195
195
  // ".coderabbit.yaml 존재"라 파일만 있고 앱이 없는 저장소에서는 아무도 요약하지 않았다.
196
- const prSummaryDir = join(commonDir, "pr-summary");
197
- if (exists(prSummaryDir) && aiPrSummary) {
196
+ // (4.8) common/projects-sync — Projects 보드 동기화 (#716). 이것도 선택했을 때만 복사한다.
197
+ // Secret(_GITHUB_PAT_TOKEN)과 PROJECT_URL 변수를 등록해야 동작하는데, Projects 를 안 쓰는 레포가
198
+ // 많아 설치만 되고 설정되지 않은 채 남았다. 같은 폴더 방식이라 게이트와 고아 감지(orphan-workflows.js)를 함께 둔다.
199
+ for (const [groupName, groupEnabled] of [["pr-summary", aiPrSummary], ["projects-sync", projectsSync]]) {
200
+ const prSummaryDir = join(commonDir, groupName);
201
+ if (exists(prSummaryDir) && groupEnabled) {
198
202
  for (const filename of listYamlFiles(prSummaryDir)) {
199
203
  const src = join(prSummaryDir, filename);
200
204
  const dst = join(workflowsDir, filename);
201
205
  if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOptsFor("common"))) {
202
206
  counters.skipped++;
203
- trace?.event("copy", "skipped-unchanged", filename, { group: "pr-summary" });
207
+ trace?.event("copy", "skipped-unchanged", filename, { group: groupName });
204
208
  continue;
205
209
  }
206
210
  const backedUp = existsSync(dst);
@@ -209,10 +213,11 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
209
213
  counters.optionalCopied++;
210
214
  counters.copied++;
211
215
  counters.copiedFiles.push(filename);
212
- trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: "pr-summary" });
216
+ trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: groupName });
213
217
  if (backedUp) counters.replacedBak.push(filename); // #673
214
218
  }
215
219
  }
220
+ }
216
221
 
217
222
  // (5) common/secret-backup — 있으면 무조건 스킵/신규만 복사
218
223
  const secretDir = join(commonDir, "secret-backup");
@@ -5,6 +5,7 @@
5
5
  export const DOCS_TO_REMOVE = [
6
6
  "CONTRIBUTING.md",
7
7
  "SECURITY.md",
8
+ "ARCHITECTURE.md",
8
9
  "CLAUDE.md",
9
10
  "AGENTS.md",
10
11
  "GEMINI.md",
@@ -14,6 +15,10 @@ export const DOCS_TO_REMOVE = [
14
15
  "pr_body.md",
15
16
  ];
16
17
 
18
+ // ⚠️ `.github/i18n`(한국어 템플릿 오버레이, #769)은 여기에 넣지 않는다. 이 목록은 내려받은 템플릿 폴더에서
19
+ // 항목을 지우는데, 오버레이는 language: ko 설치 때 그 폴더에서 읽어 와야 한다. 사용자 레포로는
20
+ // copyIssueTemplates 가 ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE 만 골라 복사하므로 새지 않는다.
21
+ // ("Use this template" 경로는 template_initializer.py 가 지운다.)
17
22
  export const PLUGIN_ITEMS_TO_REMOVE = [
18
23
  ".claude-plugin",
19
24
  ".codex-plugin",
@@ -25,8 +30,11 @@ export const PLUGIN_ITEMS_TO_REMOVE = [
25
30
  "bin",
26
31
  "src",
27
32
  "site",
33
+ ".github/CODEOWNERS",
28
34
  ".github/workflows/PROJECT-TEMPLATE-PLUGIN-VERSION-SYNC.yaml",
29
35
  ".github/workflows/PROJECT-TEMPLATE-NPM-PUBLISH.yaml",
30
36
  ".github/workflows/PROJECT-TEMPLATE-DOCS-DEPLOY.yaml",
37
+ ".github/workflows/PROJECT-TEMPLATE-SYNC-COMMUNITY-LABELS.yaml",
38
+ ".github/config/community-labels.yml",
31
39
  ];
32
40
  // ⚠️ skills/ 는 제외하지 않는다 (Cursor 설치 소스로 보존)