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.
- package/README.md +144 -145
- package/package.json +2 -2
- package/src/cli/args.js +65 -29
- package/src/cli/errors.js +9 -7
- package/src/cli/help.js +51 -3
- package/src/commands/doctor.js +80 -78
- package/src/commands/full.js +11 -2
- package/src/commands/interactive.js +71 -49
- package/src/commands/issues.js +4 -0
- package/src/commands/skills.js +8 -7
- package/src/commands/version.js +4 -2
- package/src/commands/workflows.js +3 -1
- package/src/context.js +4 -0
- package/src/core/breaking-check.js +6 -5
- package/src/core/copy/simple.js +4 -0
- package/src/core/copy/workflows.js +10 -5
- package/src/core/exclusions.js +8 -0
- package/src/core/ide/adapters/claude.js +18 -17
- package/src/core/ide/adapters/codex.js +12 -11
- package/src/core/ide/adapters/cursor.js +11 -10
- package/src/core/ide/adapters/gemini.js +11 -10
- package/src/core/ide/adapters/pi-common.js +2 -1
- package/src/core/ide/adapters/pi-harness.js +13 -16
- package/src/core/ide/adapters/pi.js +12 -11
- package/src/core/label-style.js +78 -0
- package/src/core/migration-guide.js +38 -38
- package/src/core/migrations/index.js +19 -15
- package/src/core/migrations/registry.js +70 -55
- package/src/core/options-ask.js +65 -64
- package/src/core/orphan-workflows.js +2 -1
- package/src/core/paths-resolve.js +31 -30
- package/src/core/repo-language.js +47 -0
- package/src/core/run-trace.js +10 -0
- package/src/core/version-yml.js +68 -36
- package/src/i18n/core.en.js +31 -0
- package/src/i18n/core.ko.js +31 -0
- package/src/i18n/doctor.en.js +89 -0
- package/src/i18n/doctor.ko.js +89 -0
- package/src/i18n/en.js +141 -0
- package/src/i18n/ide.en.js +102 -0
- package/src/i18n/ide.ko.js +102 -0
- package/src/i18n/index.js +73 -0
- package/src/i18n/ko.js +134 -0
- package/src/i18n/migrate.en.js +66 -0
- package/src/i18n/migrate.ko.js +66 -0
- package/src/i18n/options.en.js +104 -0
- package/src/i18n/options.ko.js +104 -0
- package/src/i18n/summary.en.js +70 -0
- package/src/i18n/summary.ko.js +70 -0
- package/src/index.js +46 -28
- package/src/ui/banner.js +8 -6
- package/src/ui/env-plan.js +19 -18
- package/src/ui/prompts.js +25 -24
- package/src/ui/readline-engine.js +18 -10
- package/src/ui/skills-prompts.js +8 -7
- package/src/ui/status-cards.js +38 -38
- package/src/ui/summary.js +103 -111
package/src/commands/full.js
CHANGED
|
@@ -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?.(
|
|
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?.("
|
|
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?.("
|
|
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?.("
|
|
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?.("
|
|
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:
|
|
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?.("
|
|
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("
|
|
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?.("
|
|
236
|
+
else io.note?.(t("flow.badVersion"), t("flow.badVersionTitle"));
|
|
215
237
|
}
|
|
216
238
|
} else if (what === "branch") {
|
|
217
|
-
const b = await io.askText("
|
|
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"),
|
|
283
|
-
const yes = await io.askYesNo(
|
|
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:
|
|
343
|
+
message: t("flow.conflictType", { type }),
|
|
322
344
|
options: [
|
|
323
|
-
{ value: "skip", label: "
|
|
324
|
-
{ value: "backup", label: ".
|
|
325
|
-
{ value: "template", label: "
|
|
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
|
|
356
|
-
|
|
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(
|
|
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?.(
|
|
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("
|
|
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
|
-
|
|
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
|
-
}
|
|
418
|
-
io.
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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(
|
|
452
|
-
lines.push(
|
|
453
|
-
lines.push(
|
|
454
|
-
lines.push(
|
|
455
|
-
lines.push(
|
|
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()) {
|
package/src/commands/issues.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/skills.js
CHANGED
|
@@ -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 || "
|
|
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}:
|
|
28
|
+
return `${label}: ${t("skills.status.installed")}${v}${versionTag(status.version, templateVersion)}`;
|
|
28
29
|
}
|
|
29
|
-
return `${label}: ${status.note || "
|
|
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("
|
|
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("
|
|
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("
|
|
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(
|
|
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;
|
package/src/commands/version.js
CHANGED
|
@@ -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(
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
80
|
+
e(t("breaking.nonInteractive"));
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
return true;
|
package/src/core/copy/simple.js
CHANGED
|
@@ -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
|
-
|
|
197
|
-
|
|
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:
|
|
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:
|
|
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");
|
package/src/core/exclusions.js
CHANGED
|
@@ -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 설치 소스로 보존)
|