projectops 4.31.5 → 4.33.1
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 +17 -3
- package/src/commands/interactive.js +78 -50
- package/src/commands/issues.js +8 -1
- 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 +27 -1
- 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 +57 -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 +142 -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 +135 -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
|
@@ -10,12 +10,15 @@ import { markerForType } from "../core/detect.js";
|
|
|
10
10
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
11
11
|
import { copyWorkflows } from "../core/copy/workflows.js";
|
|
12
12
|
import {
|
|
13
|
-
copyScripts, copyConfigFolder, copyIssueTemplates,
|
|
13
|
+
copyScripts, copyConfigFolder, copyIssueTemplates, stripIssueTemplateAssignees,
|
|
14
14
|
copyDiscussionTemplates, copySetupGuide,
|
|
15
15
|
} from "../core/copy/simple.js";
|
|
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";
|
|
21
|
+
import { t } from "../i18n/index.js";
|
|
19
22
|
import { verifyInstall } from "../core/verify.js";
|
|
20
23
|
|
|
21
24
|
// context: { version, types, paths:Map, branch, versionCode, deployTarget, publishTargets, includeSecretBackup,
|
|
@@ -26,7 +29,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
26
29
|
force = true, now, today, templateVersion = "unknown",
|
|
27
30
|
deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true,
|
|
28
31
|
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
29
|
-
deployBranch = "", intent = null, semverAuto = true , appRelease = null } = context;
|
|
32
|
+
deployBranch = "", intent = null, semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = "en" } = context;
|
|
30
33
|
|
|
31
34
|
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
32
35
|
const pathMarkers = new Map();
|
|
@@ -48,7 +51,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
48
51
|
version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
|
|
49
52
|
deployValues,
|
|
50
53
|
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, aiPrSummary, optionsDate: today,
|
|
51
|
-
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease },
|
|
54
|
+
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language },
|
|
52
55
|
})), { version, versionCode });
|
|
53
56
|
|
|
54
57
|
// 2. README 버전 섹션
|
|
@@ -66,6 +69,14 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
66
69
|
// 7. issue / discussion 템플릿
|
|
67
70
|
step("copy-templates", () => {
|
|
68
71
|
copyIssueTemplates(tempDir, targetRoot);
|
|
72
|
+
// 템플릿 언어(#769) — 원본은 영문이라 한국어 레포는 복사 직후 오버레이로 덮는다.
|
|
73
|
+
// 라벨 표기 변환(apply-label-style)보다 반드시 먼저여야 한다.
|
|
74
|
+
const { changed: overlaid, failed: overlayFailed } = applyRepoLanguage(tempDir, targetRoot, language);
|
|
75
|
+
hooks.trace?.event("copy", "repo-language", language, { overlaid: overlaid.length, failed: overlayFailed.length });
|
|
76
|
+
for (const f of overlayFailed) hooks.trace?.event("copy", "overlay-failed", f.file, { reason: f.error });
|
|
77
|
+
// 담당자 고정값 제거(#782) — 오버레이가 다시 넣으므로 그 뒤에 한다.
|
|
78
|
+
hooks.trace?.event("copy", "strip-assignees", "", { files: stripIssueTemplateAssignees(targetRoot).length });
|
|
79
|
+
if (overlayFailed.length) console.error(t("language.overlayFailed", { n: overlayFailed.length, files: overlayFailed.map((f) => f.file).join(", ") }));
|
|
69
80
|
copyDiscussionTemplates(tempDir, targetRoot);
|
|
70
81
|
});
|
|
71
82
|
|
|
@@ -76,6 +87,9 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
76
87
|
step("ensure-gitignore", () => ensureGitignore(targetRoot));
|
|
77
88
|
step("copy-setup-guide", () => copySetupGuide(tempDir, targetRoot));
|
|
78
89
|
|
|
90
|
+
// 상태 라벨 표기(#776) — 템플릿 원본은 영문이라, 한글 레포는 복사가 끝난 뒤 이름만 되돌린다.
|
|
91
|
+
step("apply-label-style", () => applyLabelStyle(targetRoot, labelStyle), { labelStyle });
|
|
92
|
+
|
|
79
93
|
// 9. 설치 후 검증 (#549) — 디스크에 쓰인 최종 결과물을 다시 읽는다.
|
|
80
94
|
// 치환은 파일 단위로 흩어져 일어나고 auto 토큰은 resolver 결과에 의존하므로,
|
|
81
95
|
// 최종 내용을 보는 것이 실제 배포될 것과 같은 것을 보는 유일한 방법이다.
|
|
@@ -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,21 +88,28 @@ 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
|
|
|
97
101
|
// issues 모드는 정보 수집 없이 바로 실행
|
|
98
102
|
if (mode === "issues") {
|
|
99
|
-
|
|
103
|
+
// 기존 레포의 템플릿 언어와 라벨 표기는 저장값을 따른다. 빈 baseCtx 로 만들면 둘 다 null 이 되어
|
|
104
|
+
// runIssues 의 기본값(en)이 적용돼, 키 없는 기존 한국어 레포가 질문 없이 영문으로 바뀐다 (#769).
|
|
105
|
+
const ctx = createContext({
|
|
106
|
+
...baseCtx, mode, force: true,
|
|
107
|
+
language: resolveRepoLanguage({ flag: null, stored: existing?.options?.language, existing: !!existing }),
|
|
108
|
+
labelStyle: resolveLabelStyle({ flag: null, stored: existing?.options?.labelStyle, existing: !!existing }),
|
|
109
|
+
});
|
|
100
110
|
runIssues(ctx, tempDir, cwd);
|
|
101
111
|
io.summary?.({ mode, types: [], version: "", counters: {} }, cwd);
|
|
102
|
-
io.outro?.("
|
|
112
|
+
io.outro?.(t("flow.doneIssues"));
|
|
103
113
|
return 0;
|
|
104
114
|
}
|
|
105
115
|
|
|
@@ -130,8 +140,26 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
130
140
|
// 저장값이 있으면 보존, 없으면 신규 통합만 ON. 기존 레포는 업데이트만으로 버전이 튀지 않는다.
|
|
131
141
|
const semverAuto = existing?.options?.semverAuto ?? (existing ? false : true);
|
|
132
142
|
const appRelease = existing?.options?.appRelease ?? null; // #553 저장값 보존 (묻지 않음)
|
|
143
|
+
// 상태 라벨 표기(#776): 저장값 > (신규 en / 기존 ko). 기존 한글 레포에는 영문 전환을 한 번 제안한다(아래).
|
|
144
|
+
const closeOnRelease = existing?.options?.closeOnRelease ?? (existing ? null : true); // #771 묻지 않는다
|
|
145
|
+
// Projects 보드 동기화(#716): 묻지 않는다. 저장값 > 이미 설치돼 있으면 유지 > 신규는 제외 (index.js 와 같은 규칙).
|
|
146
|
+
const projectsSync = existing?.options?.projectsSync
|
|
147
|
+
?? existsSync(join(cwd, ".github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml"));
|
|
148
|
+
// 레포 문구 언어(#769): 저장값 > (신규 en / 기존 ko). 기존 레포에는 영문 전환을 한 번만 제안한다.
|
|
149
|
+
let language = resolveRepoLanguage({ flag: null, stored: existing?.options?.language, existing: !!existing });
|
|
150
|
+
let labelStyle = resolveLabelStyle({ flag: null, stored: existing?.options?.labelStyle, existing: !!existing });
|
|
133
151
|
const showOptional = mode === "full" || mode === "workflows";
|
|
134
152
|
const realTty = process.stdout.isTTY === true;
|
|
153
|
+
// 기존 한글 라벨 레포에는 영문 표준 전환을 한 번만 제안한다(#776). 거절하면 한글을 저장해 다시 묻지 않는다.
|
|
154
|
+
// 라벨 정의의 from_name 이 이슈에 붙은 라벨을 그대로 옮기므로 이슈 이력은 보존된다.
|
|
155
|
+
if (existing && !existing.options?.labelStyle && realTty && showOptional && io.askYesNo) {
|
|
156
|
+
const adopt = await io.askYesNo(t("labelStyle.propose"), false);
|
|
157
|
+
labelStyle = adopt ? "en" : "ko";
|
|
158
|
+
}
|
|
159
|
+
if (existing && !existing.options?.language && realTty && showOptional && io.askYesNo) {
|
|
160
|
+
const adopt = await io.askYesNo(t("language.propose"), false);
|
|
161
|
+
language = adopt ? "en" : "ko";
|
|
162
|
+
}
|
|
135
163
|
|
|
136
164
|
// 층2 — 감지 로그 (#446)
|
|
137
165
|
io.detectionLog?.({ types, version, branch });
|
|
@@ -174,9 +202,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
174
202
|
// 업데이트 모드 — 카드는 보여주되 확인 루프 생략 (#502: 설정 변경은 기존 모드의 몫)
|
|
175
203
|
if (updateRun) {
|
|
176
204
|
if (io.analysisCard) {
|
|
177
|
-
io.analysisCard({ mode, modeLabel:
|
|
205
|
+
io.analysisCard({ mode, modeLabel: t("flow.updateScope", { scope: modeLabel(mode) }), types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, paths });
|
|
178
206
|
} else {
|
|
179
|
-
io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), "
|
|
207
|
+
io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), t("flow.title.update"));
|
|
180
208
|
}
|
|
181
209
|
confirmed = true;
|
|
182
210
|
}
|
|
@@ -185,10 +213,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
185
213
|
if (io.analysisCard) {
|
|
186
214
|
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, paths });
|
|
187
215
|
} else {
|
|
188
|
-
io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), "
|
|
216
|
+
io.note?.(summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }), t("card.title"));
|
|
189
217
|
}
|
|
190
218
|
const choice = await io.confirmProjectMenu();
|
|
191
|
-
if (choice === "cancel") { trace.event("run", "cancelled", "confirm-loop", { reason: "user-cancel" }); io.cancelMessage?.("
|
|
219
|
+
if (choice === "cancel") { trace.event("run", "cancelled", "confirm-loop", { reason: "user-cancel" }); io.cancelMessage?.(t("flow.cancelInstall")); return 0; }
|
|
192
220
|
if (isCancel(choice) || choice == null) continue; // ESC = 머무르기 (루프 재출력)
|
|
193
221
|
if (choice === "continue") { confirmed = true; break; }
|
|
194
222
|
// edit 루프
|
|
@@ -207,14 +235,14 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
207
235
|
if ([...types].sort().join(",") !== oldSorted) paths = new Map();
|
|
208
236
|
}
|
|
209
237
|
} else if (what === "version") {
|
|
210
|
-
const v = await io.askText("
|
|
238
|
+
const v = await io.askText(t("flow.askVersion"), version);
|
|
211
239
|
if (!isCancel(v) && v !== version) {
|
|
212
240
|
// semver 형식 검증 (.sh L2010~2015)
|
|
213
241
|
if (/^\d+\.\d+\.\d+$/.test(v)) version = v;
|
|
214
|
-
else io.note?.("
|
|
242
|
+
else io.note?.(t("flow.badVersion"), t("flow.badVersionTitle"));
|
|
215
243
|
}
|
|
216
244
|
} else if (what === "branch") {
|
|
217
|
-
const b = await io.askText("
|
|
245
|
+
const b = await io.askText(t("flow.askBranch"), branch);
|
|
218
246
|
if (!isCancel(b) && b) branch = b;
|
|
219
247
|
} else if (OPTION_AXES.includes(what)) {
|
|
220
248
|
// #483 — 선택한 축 하나만 재질문 (scope). 나머지 옵션은 현재값 그대로 유지.
|
|
@@ -279,8 +307,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
279
307
|
const liteCtx = { types, paths, deployTarget, publishTargets, repoName, resolvers, branch, deployBranch };
|
|
280
308
|
const conflicts = listWorkflowConflicts(liteCtx, tempDir, cwd);
|
|
281
309
|
if (conflicts.length) {
|
|
282
|
-
io.note?.(conflicts.map((c) => `• ${c.filename}`).join("\n"),
|
|
283
|
-
const yes = await io.askYesNo(
|
|
310
|
+
io.note?.(conflicts.map((c) => `• ${c.filename}`).join("\n"), t("flow.conflictsTitle", { count: conflicts.length }));
|
|
311
|
+
const yes = await io.askYesNo(t("flow.conflictsAsk", { count: conflicts.length }), true);
|
|
284
312
|
trace.event("prompt", "conflict-bulk", yes ? "backup" : "skip", { count: conflicts.length, files: conflicts.map((c) => c.filename) });
|
|
285
313
|
const decision = yes === true ? "backup" : "skip";
|
|
286
314
|
updateDecisions = new Map();
|
|
@@ -300,7 +328,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
300
328
|
const { now, today } = clock || utcNow();
|
|
301
329
|
const ctx = createContext({
|
|
302
330
|
mode, force: true, types, version, versionCode, branch, paths, deployTarget, publishTargets, includeSecretBackup,
|
|
303
|
-
codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent, semverAuto, appRelease,
|
|
331
|
+
codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch, intent, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language,
|
|
304
332
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
305
333
|
// #502 — version 모드가 기존 full 기록을 강등하지 않도록 (full이 우세)
|
|
306
334
|
recordMode: existing?.templateMode === "full" ? "full" : "version",
|
|
@@ -318,11 +346,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
318
346
|
for (const { filename, type } of conflicts) {
|
|
319
347
|
if (!perType.has(type)) {
|
|
320
348
|
const sel = await io.engineIo.select({
|
|
321
|
-
message:
|
|
349
|
+
message: t("flow.conflictType", { type }),
|
|
322
350
|
options: [
|
|
323
|
-
{ value: "skip", label: "
|
|
324
|
-
{ value: "backup", label: ".
|
|
325
|
-
{ value: "template", label: "
|
|
351
|
+
{ value: "skip", label: t("flow.opt.skip") },
|
|
352
|
+
{ value: "backup", label: t("flow.opt.backup") },
|
|
353
|
+
{ value: "template", label: t("flow.opt.template") },
|
|
326
354
|
],
|
|
327
355
|
});
|
|
328
356
|
perType.set(type, isCancel(sel) || sel == null ? "skip" : sel); // ESC = 건너뛰기 (.sh L3463)
|
|
@@ -349,13 +377,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
349
377
|
if (mode === "full" || mode === "workflows") {
|
|
350
378
|
const orphans = detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types,
|
|
351
379
|
// 껐는데 남아 계속 도는 워크플로우도 함께 잡는다 (#566)
|
|
352
|
-
options: { includeSecretBackup, aiPrSummary: aiPrSummary !== false, deployTarget } });
|
|
380
|
+
options: { includeSecretBackup, aiPrSummary: aiPrSummary !== false, projectsSync, deployTarget } });
|
|
353
381
|
if (orphans.length > 0) {
|
|
354
382
|
io.note?.(
|
|
355
|
-
orphans.map((o) => `• ${o.filename
|
|
356
|
-
|
|
383
|
+
orphans.map((o) => `• ${t("flow.orphanLine", { file: o.filename, type: o.type })}`).join("\n"),
|
|
384
|
+
t("flow.orphanTitle", { count: orphans.length }),
|
|
357
385
|
);
|
|
358
|
-
const yes = await io.askYesNo(
|
|
386
|
+
const yes = await io.askYesNo(t("flow.orphanAsk", { count: orphans.length }), true);
|
|
359
387
|
trace.event("prompt", "orphan-cleanup", yes ? "clean" : "keep", { count: orphans.length });
|
|
360
388
|
if (yes === true) {
|
|
361
389
|
const results = applyOrphanCleanup(cwd, orphans);
|
|
@@ -363,7 +391,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
363
391
|
const failed = results.filter((r) => r.action === "error");
|
|
364
392
|
orphanReport.cleaned = ok.map((r) => r.filename);
|
|
365
393
|
for (const r of ok) trace.event("orphan", "neutralized", r.filename);
|
|
366
|
-
io.note?.(
|
|
394
|
+
io.note?.(t("flow.orphanDone", { ok: ok.length, failed: failed.length ? t("flow.orphanFailed", { count: failed.length }) : "" }), t("flow.orphanDoneTitle"));
|
|
367
395
|
} else {
|
|
368
396
|
orphanReport.pending = orphans.map((o) => o.filename);
|
|
369
397
|
}
|
|
@@ -380,8 +408,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
380
408
|
if (updateRun) {
|
|
381
409
|
await skills({ templateVersion, tempDir, interactive: false, installedOnly: true });
|
|
382
410
|
} else {
|
|
383
|
-
const wantSkills = await io.askYesNo("
|
|
384
|
-
if (wantSkills === true) await skills({ templateVersion, tempDir, interactive: true });
|
|
411
|
+
const wantSkills = await io.askYesNo(t("flow.askSkills"), false);
|
|
412
|
+
if (wantSkills === true) { io.ideStatus?.(); await skills({ templateVersion, tempDir, interactive: true }); }
|
|
385
413
|
}
|
|
386
414
|
|
|
387
415
|
// 마이그레이션 기록 (#493/#494) — Layer 2/3 트레이스 파일 + Layer 1 가이드 엔트리 (full/workflows만)
|
|
@@ -395,7 +423,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
395
423
|
migrationGuidePath = appendGuideEntry(cwd, {
|
|
396
424
|
now, mode, types, repoName,
|
|
397
425
|
templateFrom: existing?.templateVersion || "", templateTo: templateVersion,
|
|
398
|
-
options: { deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup, coderabbit: codeReviewCoderabbit, changelogProvider, intent, semverAuto , appRelease },
|
|
426
|
+
options: { deploy: deployTarget, publish: publishTargets, secretBackup: includeSecretBackup, coderabbit: codeReviewCoderabbit, changelogProvider, intent, semverAuto , appRelease, labelStyle, closeOnRelease, projectsSync, language },
|
|
399
427
|
branches: { defaultBranch: branch, deployBranch, ready: deployBranchReady, created: deployBranchCreated },
|
|
400
428
|
breaking: breakingReport, migrations: migrationsResult, orphans: orphanReport,
|
|
401
429
|
events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
|
|
@@ -404,7 +432,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
404
432
|
}
|
|
405
433
|
|
|
406
434
|
// 완료 요약 (.sh print_summary L5438)
|
|
407
|
-
|
|
435
|
+
const summaryCtx = {
|
|
408
436
|
mode, types, version, deployBranch, deployBranchReady, migrationGuidePath,
|
|
409
437
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
410
438
|
skippedConflicts: result?.workflows?.skippedConflicts ?? [], // #654 병합 안내
|
|
@@ -414,8 +442,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
414
442
|
logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
|
|
415
443
|
logFile: files?.logFile ?? null,
|
|
416
444
|
traceFile: files?.traceFile ?? null,
|
|
417
|
-
}
|
|
418
|
-
io.
|
|
445
|
+
};
|
|
446
|
+
io.summary?.(summaryCtx, cwd);
|
|
447
|
+
// The run log is English only: when the screen is in another language, add an English copy of this screen.
|
|
448
|
+
if (getLang() !== "en") {
|
|
449
|
+
const buf = [];
|
|
450
|
+
withLang("en", () => printSummary(summaryCtx, cwd, (x) => buf.push(x)));
|
|
451
|
+
trace.appendText(buf.join(""));
|
|
452
|
+
}
|
|
453
|
+
io.outro?.(t("flow.done", { mode }));
|
|
419
454
|
|
|
420
455
|
// 완료 화면까지 출력한 뒤 finally에서 닫는다 (#561) — 여기서 닫으면 이 아래 화면이 안 담긴다.
|
|
421
456
|
trace.event("run", "end", mode || "", {
|
|
@@ -442,30 +477,23 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
442
477
|
|
|
443
478
|
function summarize({ mode, types, version, branch, deployTarget, publishTargets, includeSecretBackup, showOptional, changelogProvider, codeReviewCoderabbit }) {
|
|
444
479
|
const lines = [
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
480
|
+
`${t("sum.mode")}: ${modeLabel(mode)}`,
|
|
481
|
+
`${t("sum.types")}: ${types.join(", ")}${types.length > 1 ? t("sum.multi") : ""}`,
|
|
482
|
+
`${t("card.version")}: ${version}`,
|
|
483
|
+
`${t("edit.branch")}: ${branch}`,
|
|
449
484
|
];
|
|
450
485
|
if (showOptional) {
|
|
451
|
-
lines.push(
|
|
452
|
-
lines.push(
|
|
453
|
-
lines.push(
|
|
454
|
-
lines.push(
|
|
455
|
-
lines.push(
|
|
486
|
+
lines.push(`${t("sum.deploy")}: ${deployTarget || "docker-ssh"}`);
|
|
487
|
+
lines.push(`${t("sum.publish")}: ${(publishTargets ?? []).join(",") || t("card.none")}`);
|
|
488
|
+
lines.push(`${t("sum.secret")}: ${includeSecretBackup ? t("card.included") : t("card.excluded")}`);
|
|
489
|
+
lines.push(`${t("sum.changelog")}: ${changelogProvider || "commit"}`);
|
|
490
|
+
lines.push(`${t("sum.coderabbit")}: ${codeReviewCoderabbit ? t("sum.on") : t("sum.off")}`);
|
|
456
491
|
}
|
|
457
492
|
return lines.join("\n");
|
|
458
493
|
}
|
|
459
494
|
|
|
460
495
|
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;
|
|
496
|
+
return m === "update" ? t("mode.update.short") : (t(`mode.${m}`) === `mode.${m}` ? m : t(`mode.${m}`));
|
|
469
497
|
}
|
|
470
498
|
|
|
471
499
|
function utcNow(date = new Date()) {
|
package/src/commands/issues.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// issues 모드 (.sh execute_integration issues case 등가) — template_integrator.sh 4532~4535.
|
|
2
2
|
// 이슈/PR 템플릿 + Discussion 템플릿만 복사.
|
|
3
|
-
import {
|
|
3
|
+
import { applyLabelStyle } from "../core/label-style.js";
|
|
4
|
+
import { applyRepoLanguage } from "../core/repo-language.js";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
6
|
+
import { copyIssueTemplates, copyDiscussionTemplates, stripIssueTemplateAssignees } from "../core/copy/simple.js";
|
|
4
7
|
|
|
5
8
|
export function runIssues(context, tempDir, targetRoot = ".") {
|
|
6
9
|
copyIssueTemplates(tempDir, targetRoot);
|
|
10
|
+
const { failed } = applyRepoLanguage(tempDir, targetRoot, context.language ?? "en"); // #769 — 라벨 변환보다 먼저
|
|
11
|
+
if (failed.length) console.error(t("language.overlayFailed", { n: failed.length, files: failed.map((f) => f.file).join(", ") }));
|
|
12
|
+
stripIssueTemplateAssignees(targetRoot); // #782 — 오버레이가 다시 넣으므로 그 뒤에
|
|
13
|
+
applyLabelStyle(targetRoot, context.labelStyle ?? "en"); // #776
|
|
7
14
|
copyDiscussionTemplates(tempDir, targetRoot);
|
|
8
15
|
}
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 단순 복사 함수 (무조건 덮어쓰기류) — .sh copy_scripts/config/issue/discussion/setup_guide 등가.
|
|
2
2
|
// 실측: template_integrator.sh 3818, 3845, 3872, 3895, 4114.
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { chmodSync } from "node:fs";
|
|
4
|
+
import { chmodSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { PATHS } from "../paths.js";
|
|
6
6
|
import { exists, copyFileSync, copyDirSync } from "../fsutil.js";
|
|
7
7
|
|
|
@@ -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",
|
|
@@ -56,6 +60,28 @@ export function copyConfigFolder(tempDir, targetRoot = ".") {
|
|
|
56
60
|
return true;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
// 사용자 레포로 복사된 이슈 템플릿에서 담당자(assignees) 줄을 지운다 (#782).
|
|
64
|
+
// 원본에는 이 저장소 소유자가 박혀 있는데, 남의 레포에서 새 이슈를 만들 때 그 사람이 담당자로
|
|
65
|
+
// 지정되면 안 된다. 담당자는 이슈 헬퍼와 QA 봇이 version.yml 설정으로 정한다.
|
|
66
|
+
// frontmatter(첫 --- 와 둘째 --- 사이)의 줄만 대상이다 — 본문에 같은 문구가 있어도 건드리지 않는다.
|
|
67
|
+
// 반환: 바뀐 파일의 레포 루트 기준 상대경로. 오버레이 적용 뒤에 불러야 한다(오버레이가 다시 넣는다).
|
|
68
|
+
export function stripIssueTemplateAssignees(targetRoot = ".") {
|
|
69
|
+
const dir = join(targetRoot, ".github", "ISSUE_TEMPLATE");
|
|
70
|
+
if (!exists(dir)) return [];
|
|
71
|
+
const changed = [];
|
|
72
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith(".md"))) {
|
|
73
|
+
const file = join(dir, name);
|
|
74
|
+
const text = readFileSync(file, "utf8");
|
|
75
|
+
const m = text.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
|
76
|
+
if (!m) continue;
|
|
77
|
+
const front = m[2].split(/\r?\n/).filter((l) => !/^assignees:/.test(l)).join("\n");
|
|
78
|
+
if (front === m[2]) continue;
|
|
79
|
+
writeFileSync(file, text.replace(m[0], `${m[1]}${front}${m[3]}`));
|
|
80
|
+
changed.push(`.github/ISSUE_TEMPLATE/${name}`);
|
|
81
|
+
}
|
|
82
|
+
return changed;
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
// .github/ISSUE_TEMPLATE/ 전체 + PULL_REQUEST_TEMPLATE.md 덮어쓰기.
|
|
60
86
|
export function copyIssueTemplates(tempDir, targetRoot = ".") {
|
|
61
87
|
const srcIssue = join(tempDir, ".github", "ISSUE_TEMPLATE");
|