projectops 4.6.0 → 4.7.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 +1 -1
- package/package.json +1 -1
- package/src/cli/args.js +4 -0
- package/src/cli/help.js +1 -0
- package/src/commands/full.js +3 -3
- package/src/commands/interactive.js +19 -14
- package/src/commands/version.js +1 -1
- package/src/context.js +2 -1
- package/src/core/copy/simple.js +3 -1
- package/src/core/copy/workflows.js +25 -2
- package/src/core/options-ask.js +50 -35
- package/src/core/orphan-workflows.js +40 -2
- package/src/core/version-yml.js +7 -2
- package/src/index.js +44 -44
- package/src/ui/summary.js +13 -17
package/README.md
CHANGED
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -17,6 +17,7 @@ export function parseArgs(argv) {
|
|
|
17
17
|
deployBranch: "", // 릴리스 PR head 브랜치 (#456): --deploy-branch, 빈 값=미지정
|
|
18
18
|
intent: null, // 프로젝트 성격 (#485): --intent app|library|both|none|manual, null=미설정(역추론)
|
|
19
19
|
includeSecretBackup: null,
|
|
20
|
+
aiPrSummary: null, // #566 — AI 변경 요약 워크플로우 포함 여부
|
|
20
21
|
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
21
22
|
force: false,
|
|
22
23
|
help: false,
|
|
@@ -101,6 +102,9 @@ export function parseArgs(argv) {
|
|
|
101
102
|
break;
|
|
102
103
|
case "--secret-backup": result.includeSecretBackup = true; break;
|
|
103
104
|
case "--no-secret-backup": result.includeSecretBackup = false; break;
|
|
105
|
+
// #566 — 비대화형에서도 켜고 끌 수 있어야 한다. 없으면 자동화 환경은 선택권이 없다.
|
|
106
|
+
case "--ai-summary": result.aiPrSummary = true; break;
|
|
107
|
+
case "--no-ai-summary": result.aiPrSummary = false; break;
|
|
104
108
|
case "--npm-publish":
|
|
105
109
|
process.stderr.write("⚠️ --npm-publish는 deprecated입니다. --publish npm 을 사용하세요.\n");
|
|
106
110
|
result.publishTargets = [...new Set([...(result.publishTargets ?? []), "npm"])];
|
package/src/cli/help.js
CHANGED
|
@@ -20,6 +20,7 @@ export const HELP_TEXT = `projectops — GitHub 프로젝트 자동화 템플릿
|
|
|
20
20
|
--publish CSV publish 타겟 csv: nexus,npm,github-packages (기본: 없음)
|
|
21
21
|
--deploy-branch NAME 릴리스 PR head 브랜치 (#456, 기본: develop). default_branch와 별개
|
|
22
22
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
23
|
+
--ai-summary / --no-ai-summary PR 변경 요약 워크플로우 포함/제외
|
|
23
24
|
--nexus / --npm-publish (deprecated — --publish nexus / --publish npm 사용)
|
|
24
25
|
--force 모든 확인 생략, 비대화형 기본값 사용
|
|
25
26
|
-v, --version projectops 버전 출력
|
package/src/commands/full.js
CHANGED
|
@@ -23,8 +23,8 @@ import { verifyInstall } from "../core/verify.js";
|
|
|
23
23
|
export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
24
24
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
25
25
|
force = true, now, today, templateVersion = "unknown",
|
|
26
|
-
deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false,
|
|
27
|
-
changelogProvider = "
|
|
26
|
+
deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true,
|
|
27
|
+
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
28
28
|
deployBranch = "", intent = null, semverAuto = true , appRelease = null } = context;
|
|
29
29
|
|
|
30
30
|
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
@@ -43,7 +43,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
43
43
|
buildVersionYml({
|
|
44
44
|
version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
|
|
45
45
|
deployValues,
|
|
46
|
-
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate: today,
|
|
46
|
+
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, aiPrSummary, optionsDate: today,
|
|
47
47
|
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease },
|
|
48
48
|
})), { version, versionCode });
|
|
49
49
|
|
|
@@ -13,7 +13,7 @@ import { runBreakingCheck } from "../core/breaking-check.js";
|
|
|
13
13
|
import { runMigrations } from "../core/migrations/index.js";
|
|
14
14
|
import { detectOrphanWorkflows, applyOrphanCleanup } from "../core/orphan-workflows.js";
|
|
15
15
|
import { resolveProjectPaths, filterExcludedTypes } from "../core/paths-resolve.js";
|
|
16
|
-
import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets } from "../core/options-ask.js";
|
|
16
|
+
import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets, migrateProvider } from "../core/options-ask.js";
|
|
17
17
|
import { createRunTrace, MIGRATION_DIR } from "../core/run-trace.js";
|
|
18
18
|
import { appendGuideEntry } from "../core/migration-guide.js";
|
|
19
19
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
@@ -41,6 +41,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
41
41
|
// finally에서 기록할 때 쓰는 값 — try 안에서 확정되기 전에 취소될 수 있으므로 바깥에 둔다 (#561).
|
|
42
42
|
let traceFrom = "";
|
|
43
43
|
let traceTo = "";
|
|
44
|
+
// 기록 파일을 남기는 모드인지 — 모드 선택 전에 취소될 수 있어 기본값은 false.
|
|
45
|
+
let recordArtifacts = false;
|
|
46
|
+
// 기록을 남기지 않고 끝낼 경우(version/issues 모드의 정상 완주)에만 true.
|
|
47
|
+
// 취소·예외는 모드와 무관하게 남긴다 — 끊겼을 때가 "어디까지 갔는지"가 가장 궁금한 순간이다.
|
|
48
|
+
let skipRecord = false;
|
|
44
49
|
if (io === prompts) trace.mirrorStart();
|
|
45
50
|
try {
|
|
46
51
|
// 템플릿 먼저 획득 — 배너에 실제 템플릿 버전을 표시 (.sh는 원격 version.yml fetch L4270~4280 등가)
|
|
@@ -113,8 +118,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
113
118
|
let deployTarget = existing?.options?.deploy ?? "docker-ssh";
|
|
114
119
|
let publishTargets = existing?.options?.publish ?? [];
|
|
115
120
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
121
|
+
let aiPrSummary = existing?.options?.aiPrSummary ?? null; // null = 아직 안 물음 (#566)
|
|
116
122
|
let codeReviewCoderabbit = existing?.options?.codeReviewCoderabbit ?? true;
|
|
117
|
-
let changelogProvider = existing?.options?.changelogProvider ?? "
|
|
123
|
+
let changelogProvider = migrateProvider(existing?.options?.changelogProvider) ?? "commit";
|
|
118
124
|
let changelogBaseUrl = existing?.options?.changelogBaseUrl ?? "";
|
|
119
125
|
let deployBranch = existing?.options?.deployBranch ?? "develop"; // #456
|
|
120
126
|
let deployBranchReady = null; // #490 — 이번 실행에서 개발 브랜치 존재/생성이 확인됐는지
|
|
@@ -341,7 +347,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
341
347
|
// 고아 타입 워크플로우 정리 (#487) — 타입 변경으로 선택에서 빠진 타입의 잔존 워크플로우
|
|
342
348
|
const orphanReport = { cleaned: [], pending: [] }; // #493 — 가이드 기록용
|
|
343
349
|
if (mode === "full" || mode === "workflows") {
|
|
344
|
-
const orphans = detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types
|
|
350
|
+
const orphans = detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types,
|
|
351
|
+
// 껐는데 남아 계속 도는 워크플로우도 함께 잡는다 (#566)
|
|
352
|
+
options: { includeSecretBackup, aiPrSummary: aiPrSummary !== false, deployTarget } });
|
|
345
353
|
if (orphans.length > 0) {
|
|
346
354
|
io.note?.(
|
|
347
355
|
orphans.map((o) => `• ${o.filename} (${o.type} 타입 — 현재 미선택)`).join("\n"),
|
|
@@ -378,7 +386,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
378
386
|
|
|
379
387
|
// 마이그레이션 기록 (#493/#494) — Layer 2/3 트레이스 파일 + Layer 1 가이드 엔트리 (full/workflows만)
|
|
380
388
|
let migrationGuidePath = null;
|
|
381
|
-
|
|
389
|
+
recordArtifacts = mode === "full" || mode === "workflows";
|
|
382
390
|
// 경로는 먼저 계산하고 실제 쓰기는 완료 화면 뒤로 미룬다 — 요약까지 터미널 미러에 담기 위함 (#561)
|
|
383
391
|
const files = recordArtifacts
|
|
384
392
|
? trace.paths({ fromVersion: existing?.templateVersion || "", toVersion: templateVersion, now })
|
|
@@ -406,16 +414,12 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
406
414
|
}, cwd);
|
|
407
415
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
408
416
|
|
|
409
|
-
// 완료 화면까지
|
|
417
|
+
// 완료 화면까지 출력한 뒤 finally에서 닫는다 (#561) — 여기서 닫으면 이 아래 화면이 안 담긴다.
|
|
410
418
|
trace.event("run", "end", mode || "", {
|
|
411
419
|
workflowsCopied: result?.workflows?.copied ?? 0,
|
|
412
420
|
workflowsSkipped: result?.workflows?.skipped ?? 0,
|
|
413
421
|
});
|
|
414
|
-
|
|
415
|
-
trace.finalize({ targetRoot: cwd, fromVersion: existing?.templateVersion || "", toVersion: templateVersion, now });
|
|
416
|
-
} else {
|
|
417
|
-
trace.mirrorStop();
|
|
418
|
-
}
|
|
422
|
+
skipRecord = !recordArtifacts;
|
|
419
423
|
return 0;
|
|
420
424
|
} catch (err) {
|
|
421
425
|
trace.event("run", "error", "interactive", {
|
|
@@ -424,9 +428,10 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
424
428
|
});
|
|
425
429
|
throw err;
|
|
426
430
|
} finally {
|
|
427
|
-
// 어떤 경로로 빠져나가든 기록을 남긴다 (#561) — 중간
|
|
428
|
-
//
|
|
429
|
-
trace.
|
|
431
|
+
// 어떤 경로로 빠져나가든 기록을 남긴다 (#561) — 중간 취소·예외·Ctrl+C 포함.
|
|
432
|
+
// 완료 화면 출력이 try 안에 있어야 그 화면까지 미러에 담긴 채로 여기서 닫힌다.
|
|
433
|
+
if (skipRecord) trace.mirrorStop();
|
|
434
|
+
else trace.finalize({ targetRoot: cwd, fromVersion: traceFrom, toVersion: traceTo, now: clock?.now || "" });
|
|
430
435
|
disarmSignals();
|
|
431
436
|
remove(tempDir);
|
|
432
437
|
}
|
|
@@ -443,7 +448,7 @@ function summarize({ mode, types, version, branch, deployTarget, publishTargets,
|
|
|
443
448
|
lines.push(`배포 방식 : ${deployTarget || "docker-ssh"}`);
|
|
444
449
|
lines.push(`Publish : ${(publishTargets ?? []).join(",") || "없음"}`);
|
|
445
450
|
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
446
|
-
lines.push(`Changelog : ${changelogProvider || "
|
|
451
|
+
lines.push(`Changelog : ${changelogProvider || "commit"}`);
|
|
447
452
|
lines.push(`CodeRabbit 리뷰 : ${codeReviewCoderabbit ? "사용" : "미사용"}`);
|
|
448
453
|
}
|
|
449
454
|
return lines.join("\n");
|
package/src/commands/version.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
|
13
13
|
export function runVersion(context, tempDir, targetRoot = ".") {
|
|
14
14
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
15
15
|
now, today, templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false,
|
|
16
|
-
changelogProvider = "
|
|
16
|
+
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
17
17
|
deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null } = context;
|
|
18
18
|
|
|
19
19
|
const pathMarkers = new Map();
|
package/src/context.js
CHANGED
|
@@ -19,8 +19,9 @@ 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
|
+
aiPrSummary: null, // #566 — AI 변경 요약 워크플로우 포함 여부
|
|
22
23
|
// changelog provider 축 (#455 — null=미설정)
|
|
23
|
-
changelogProvider: null, // '
|
|
24
|
+
changelogProvider: null, // 'commit'(기본) | 'coderabbit' | 'openai' | 'gemini' | 'claude' | 'ollama' | 'commit'
|
|
24
25
|
changelogBaseUrl: null, // ollama일 때만 값
|
|
25
26
|
codeReviewCoderabbit: null,
|
|
26
27
|
deployBranch: "", // 릴리스 PR head 브랜치 (#456). 빈 값=metadata.deploy_branch 미출력
|
package/src/core/copy/simple.js
CHANGED
|
@@ -20,8 +20,10 @@ export function copyScripts(tempDir, targetRoot = ".") {
|
|
|
20
20
|
"dispatch_downstream.py",
|
|
21
21
|
// AI PR SUMMARY 워크플로우가 요약 댓글을 작성·갱신할 때 호출 (#553).
|
|
22
22
|
"pr_summary_comment.py",
|
|
23
|
+
// 릴리스 노트가 어떤 경로로 만들어졌는지 알리고, AI를 못 썼을 때 대안을 안내 (#566).
|
|
24
|
+
"changelog_notice.py",
|
|
23
25
|
"changelog_providers/_common.py", "changelog_providers/ladder.py",
|
|
24
|
-
"changelog_providers/commit.py", "changelog_providers/
|
|
26
|
+
"changelog_providers/commit.py", "changelog_providers/copilot.py",
|
|
25
27
|
"changelog_providers/openai_compatible.py",
|
|
26
28
|
];
|
|
27
29
|
let copied = 0;
|
|
@@ -63,14 +63,14 @@ function classify(srcDir, workflowsDir, envOpts, baseline = null) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// copy_workflows 본체 (동기 — 기존 호출부 무변경).
|
|
66
|
-
// context: { types:[], paths:Map, deployTarget, publishTargets:[], includeSecretBackup, force, repoName, resolvers,
|
|
66
|
+
// context: { types:[], paths:Map, deployTarget, publishTargets:[], includeSecretBackup, aiPrSummary, force, repoName, resolvers,
|
|
67
67
|
// envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
|
|
68
68
|
// deployTarget(#439 택1): 'docker-ssh'(기본) | 'vercel' | 'none' — server-deploy는 docker-ssh일 때만,
|
|
69
69
|
// common/deploy/<target>/은 해당 타겟일 때 복사. publishTargets(#439 다중): 'nexus'|'npm'|'github-packages'.
|
|
70
70
|
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'> } — 기존 파일(changed) 충돌 결정.
|
|
71
71
|
// 반환: {copied, skipped, templateAdded, optionalCopied, copiedFiles[]} — copiedFiles는 실제 복사·교체된 파일명 (#473 요약용)
|
|
72
72
|
export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
73
|
-
const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
|
|
73
|
+
const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
|
|
74
74
|
const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
|
|
75
75
|
const trace = hooks.trace ?? null; // #494 — 실행 트레이스 (null-safe: 미주입이면 전 이벤트 no-op)
|
|
76
76
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
@@ -164,6 +164,29 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// (4.7) common/pr-summary — AI 변경 요약 (#566). 선택했을 때만 복사한다.
|
|
168
|
+
// 종전에는 common 본체에 있어 무조건 복사된 뒤 런타임에 스스로 빠졌고, 그 판단이
|
|
169
|
+
// ".coderabbit.yaml 존재"라 파일만 있고 앱이 없는 저장소에서는 아무도 요약하지 않았다.
|
|
170
|
+
const prSummaryDir = join(commonDir, "pr-summary");
|
|
171
|
+
if (exists(prSummaryDir) && aiPrSummary) {
|
|
172
|
+
for (const filename of listYamlFiles(prSummaryDir)) {
|
|
173
|
+
const src = join(prSummaryDir, filename);
|
|
174
|
+
const dst = join(workflowsDir, filename);
|
|
175
|
+
if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOptsFor("common"))) {
|
|
176
|
+
counters.skipped++;
|
|
177
|
+
trace?.event("copy", "skipped-unchanged", filename, { group: "pr-summary" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const backedUp = existsSync(dst);
|
|
181
|
+
if (backedUp) renameSync(dst, dst + ".bak");
|
|
182
|
+
copyFileSync(src, dst);
|
|
183
|
+
counters.optionalCopied++;
|
|
184
|
+
counters.copied++;
|
|
185
|
+
counters.copiedFiles.push(filename);
|
|
186
|
+
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: "pr-summary" });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
167
190
|
// (5) common/secret-backup — 있으면 무조건 스킵/신규만 복사
|
|
168
191
|
const secretDir = join(commonDir, "secret-backup");
|
|
169
192
|
if (exists(secretDir) && includeSecretBackup) {
|
package/src/core/options-ask.js
CHANGED
|
@@ -92,8 +92,19 @@ export function applicableTargets(types = []) {
|
|
|
92
92
|
publish: PUBLISH_TARGETS.filter((t) => types.some((ty) => (TYPE_PUBLISH_TARGETS[ty] ?? PUBLISH_TARGETS).includes(t))),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
95
|
+
// changelog 생성기 provider (#455, #566에서 재정비).
|
|
96
|
+
// 기본값은 commit — 외부 의존이 없어 어떤 환경에서도 결과가 나온다. AI를 쓰려면
|
|
97
|
+
// MODEL_API_KEY를 등록하면 사다리가 자동으로 집어 쓴다(마법사는 묻지 않는다).
|
|
98
|
+
// github-ai는 GitHub Models 종료(2026-07-30)로 제외됐다.
|
|
99
|
+
export const CHANGELOG_PROVIDERS = ["copilot", "coderabbit", "openai", "gemini", "claude", "groq", "mistral", "ollama", "commit"];
|
|
100
|
+
|
|
101
|
+
// 더 이상 동작하지 않는 저장값을 살아있는 값으로 옮긴다 (#566).
|
|
102
|
+
// 기존 저장소가 업데이트만 돌려도 죽은 설정에서 벗어나게 하는 유일한 경로다.
|
|
103
|
+
const RETIRED_PROVIDERS = { "github-ai": "commit" };
|
|
104
|
+
export function migrateProvider(saved) {
|
|
105
|
+
if (saved == null) return null;
|
|
106
|
+
return RETIRED_PROVIDERS[saved] ?? saved;
|
|
107
|
+
}
|
|
97
108
|
|
|
98
109
|
const isCancel = (v) => typeof v === "symbol";
|
|
99
110
|
|
|
@@ -140,7 +151,7 @@ const INTENT_ASKS_PUBLISH = { library: true, both: true, manual: true, app: fals
|
|
|
140
151
|
// current: { deploy: string|null, publish: string[]|null, secretBackup: bool|null } — CLI 명시값
|
|
141
152
|
// scope: null이면 전 축(초기 통합·전체 재질문). Set/배열이면 그 축만 forceAsk 대상 (#483 수정 메뉴 격리).
|
|
142
153
|
// 스코프 밖 축은 forceAsk여도 current/저장값을 그대로 유지하고 다시 묻지 않는다.
|
|
143
|
-
// 반환: { deploy, publish, secretBackup, codeReviewCoderabbit, changelogProvider, changelogBaseUrl, deployBranch }
|
|
154
|
+
// 반환: { deploy, publish, secretBackup, codeReviewCoderabbit, aiPrSummary, changelogProvider, changelogBaseUrl, deployBranch }
|
|
144
155
|
export async function askAllOptionalWorkflows({
|
|
145
156
|
tempDir, types = [], current = {}, targetRoot = ".",
|
|
146
157
|
force = false, tty = true, io = {}, forceAsk = false, defaultBranch = "", scope = null,
|
|
@@ -153,6 +164,7 @@ export async function askAllOptionalWorkflows({
|
|
|
153
164
|
let publish = current.publish ?? null;
|
|
154
165
|
let secretBackup = current.secretBackup ?? null;
|
|
155
166
|
let codeReviewCoderabbit = current.codeReviewCoderabbit ?? null;
|
|
167
|
+
let aiPrSummary = current.aiPrSummary ?? null;
|
|
156
168
|
let changelogProvider = current.changelogProvider ?? null;
|
|
157
169
|
let changelogBaseUrl = current.changelogBaseUrl ?? null;
|
|
158
170
|
let deployBranch = current.deployBranch ?? null; // #456 릴리스 PR head 브랜치
|
|
@@ -187,6 +199,7 @@ export async function askAllOptionalWorkflows({
|
|
|
187
199
|
}
|
|
188
200
|
// #455 changelog/code_review 저장값 재사용
|
|
189
201
|
if (codeReviewCoderabbit === null && saved.codeReviewCoderabbit !== null) codeReviewCoderabbit = saved.codeReviewCoderabbit;
|
|
202
|
+
if (aiPrSummary === null && saved.aiPrSummary != null) aiPrSummary = saved.aiPrSummary;
|
|
190
203
|
if (changelogProvider === null && saved.changelogProvider !== null) changelogProvider = saved.changelogProvider;
|
|
191
204
|
if (changelogBaseUrl === null && saved.changelogBaseUrl !== null) changelogBaseUrl = saved.changelogBaseUrl;
|
|
192
205
|
// #456 deploy_branch 저장값 재사용
|
|
@@ -313,19 +326,36 @@ export async function askAllOptionalWorkflows({
|
|
|
313
326
|
}
|
|
314
327
|
}
|
|
315
328
|
|
|
316
|
-
// ──
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
// ── pr_comment: PR에 달 리뷰·요약 (#566에서 통합) ──
|
|
330
|
+
// 종전에는 CodeRabbit 여부만 예/아니오로 물었고, AI 변경 요약은 물어보지도 않은 채
|
|
331
|
+
// 항상 복사된 뒤 런타임에 스스로 빠졌다. 그 결과 "CodeRabbit도 안 달고 요약도 안 다는"
|
|
332
|
+
// 저장소가 생겼다. 둘은 같은 자리(PR 댓글)를 놓고 경쟁하므로 한 번에 고르게 한다.
|
|
333
|
+
if (ask("code-review") || codeReviewCoderabbit === null || aiPrSummary === null) {
|
|
334
|
+
if (force || !tty || typeof io.select !== "function") {
|
|
335
|
+
// 비대화형 기본값: CodeRabbit은 외부 앱 설치가 필요하므로 끄고, 자체 요약만 켠다.
|
|
319
336
|
codeReviewCoderabbit = codeReviewCoderabbit ?? false;
|
|
337
|
+
aiPrSummary = aiPrSummary ?? true;
|
|
320
338
|
} else {
|
|
321
339
|
say("");
|
|
322
|
-
say("
|
|
323
|
-
const ans = await io.
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
say("💬 PR에 리뷰·요약 댓글을 어떻게 받으시겠어요?");
|
|
341
|
+
const ans = await io.select({
|
|
342
|
+
message: "PR 댓글 방식을 선택하세요",
|
|
343
|
+
options: [
|
|
344
|
+
{ value: "summary", label: "AI 변경 요약 (추천 · 추가 설정 불필요)" },
|
|
345
|
+
{ value: "coderabbit", label: "CodeRabbit 코드 리뷰 (외부 앱 설치 필요)" },
|
|
346
|
+
{ value: "both", label: "둘 다" },
|
|
347
|
+
{ value: "none", label: "사용 안 함" },
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
// 취소·미응답은 추천값으로 — 다른 축과 같은 방어 패턴이다.
|
|
351
|
+
const PR_COMMENT_CHOICES = ["summary", "coderabbit", "both", "none"];
|
|
352
|
+
const pick = (!isCancel(ans) && PR_COMMENT_CHOICES.includes(ans)) ? ans : "summary";
|
|
353
|
+
codeReviewCoderabbit = pick === "coderabbit" || pick === "both";
|
|
354
|
+
aiPrSummary = pick === "summary" || pick === "both";
|
|
355
|
+
say(`PR 댓글: ${{ summary: "AI 변경 요약", coderabbit: "CodeRabbit", both: "둘 다", none: "사용 안 함" }[pick]}`);
|
|
326
356
|
// #481 — "사용"만으로는 안 붙는다. 앱 설치 + 레포 접근 권한이 있어야 실제로 리뷰가 달린다.
|
|
327
357
|
if (codeReviewCoderabbit) {
|
|
328
|
-
say(" ⚠️
|
|
358
|
+
say(" ⚠️ CodeRabbit은 추가 설정이 필요합니다:");
|
|
329
359
|
say(" 1) https://coderabbit.ai 접속 → GitHub으로 로그인");
|
|
330
360
|
say(" 2) CodeRabbit GitHub 앱 설치 → 이 저장소에 접근 권한(grant access) 부여");
|
|
331
361
|
say(" (이 단계를 안 하면 워크플로우는 켜져도 PR에 리뷰 댓글이 달리지 않습니다)");
|
|
@@ -333,29 +363,12 @@ export async function askAllOptionalWorkflows({
|
|
|
333
363
|
}
|
|
334
364
|
}
|
|
335
365
|
|
|
336
|
-
// ── changelog:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
say("📝 릴리스 노트(changelog)는 1순위로 뭘로 만들까요?");
|
|
343
|
-
say(" GitHub AI는 설정 없이 바로 됩니다. 나머지는 나중에 GitHub Secret 등록이 필요할 수 있어요.");
|
|
344
|
-
// #481 — 하나만 골라야 하는 게 아니다. 고른 게 실패하면 자동 폴백하므로 안심하고 고르라고 안내.
|
|
345
|
-
say(" ✅ 고른 방식이 실패해도 자동으로 GitHub AI → 커밋 분석 순으로 폴백하니 릴리스 노트는 항상 생성됩니다.");
|
|
346
|
-
const ans = await io.select({
|
|
347
|
-
message: "1순위 changelog 생성기를 선택하세요 (실패 시 자동 폴백)",
|
|
348
|
-
options: [
|
|
349
|
-
{ value: "github-ai", label: "GitHub AI (추천 · 설정 불필요)" },
|
|
350
|
-
{ value: "coderabbit", label: "CodeRabbit" },
|
|
351
|
-
{ value: "openai", label: "OpenAI 호환 API (키 등록 필요)" },
|
|
352
|
-
{ value: "commit", label: "커밋 분석만 (AI 없음 · 최후 안전망)" },
|
|
353
|
-
],
|
|
354
|
-
});
|
|
355
|
-
changelogProvider = (!isCancel(ans) && CHANGELOG_PROVIDERS.includes(ans)) ? ans : (changelogProvider ?? "github-ai");
|
|
356
|
-
say(`changelog 생성기: ${changelogProvider}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
366
|
+
// ── changelog: 생성기를 묻지 않는다 (#566) ──
|
|
367
|
+
// 사용자의 목적은 "릴리스 노트가 잘 나오는 것"이지 provider 선택이 아니다. 무엇을 고를지
|
|
368
|
+
// 알 수 없는 질문이었고 기본값(github-ai)마저 죽어 있었다. 이제 워크플로우가 스스로
|
|
369
|
+
// 최선을 찾는다 — PR 본문 존중 → Copilot → 외부 AI(키 있으면) → 커밋 분석.
|
|
370
|
+
// 저장값은 그대로 두되 죽은 값만 살아있는 것으로 옮긴다.
|
|
371
|
+
changelogProvider = migrateProvider(changelogProvider) ?? "commit";
|
|
359
372
|
|
|
360
373
|
// ollama 선택 시에만 base_url 질문 (나머지 provider는 preset base_url 자동 — #455)
|
|
361
374
|
if (changelogProvider === "ollama" && (ask("changelog") || changelogBaseUrl === null || changelogBaseUrl === "")) {
|
|
@@ -408,7 +421,9 @@ export async function askAllOptionalWorkflows({
|
|
|
408
421
|
return {
|
|
409
422
|
deploy: finalDeploy, publish: finalPublish, secretBackup: secretBackup === true,
|
|
410
423
|
codeReviewCoderabbit: codeReviewCoderabbit === true,
|
|
411
|
-
|
|
424
|
+
// #566 — AI 변경 요약 워크플로우 포함 여부. 기본 true(추가 설정 없이 바로 동작).
|
|
425
|
+
aiPrSummary: aiPrSummary !== false,
|
|
426
|
+
changelogProvider: changelogProvider ?? "commit",
|
|
412
427
|
changelogBaseUrl: changelogBaseUrl ?? "",
|
|
413
428
|
deployBranch: deployBranch ?? "develop",
|
|
414
429
|
deployBranchReady, // #490 — true=존재/생성 확인됨, false=거절/실패, null=확인 안 함
|
|
@@ -25,8 +25,38 @@ function typeInventory(projectTypesDir, type) {
|
|
|
25
25
|
return files;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
|
|
28
|
+
// common/ 아래 "선택했을 때만 복사되는" 폴더들 (#566).
|
|
29
|
+
// 켜면 복사 엔진이 넣어주지만, 끄면 이미 있던 파일이 그대로 남아 계속 실행됐다.
|
|
30
|
+
// 축을 끈 사용자에게는 그게 곧 "껐는데 왜 도나"가 된다 — 여기서 고아로 잡는다.
|
|
31
|
+
// deploy는 타겟별 폴더라 선택된 타겟만 살리고 나머지를 대상으로 삼는다.
|
|
32
|
+
function commonOptionalOrphans(commonDir, opts) {
|
|
33
|
+
const { includeSecretBackup = false, aiPrSummary = true, deployTarget = "docker-ssh" } = opts;
|
|
34
|
+
const out = [];
|
|
35
|
+
|
|
36
|
+
const gated = [
|
|
37
|
+
["secret-backup", includeSecretBackup, "secret-backup"],
|
|
38
|
+
["pr-summary", aiPrSummary, "pr-summary"],
|
|
39
|
+
];
|
|
40
|
+
for (const [dir, enabled, label] of gated) {
|
|
41
|
+
if (enabled) continue;
|
|
42
|
+
const d = join(commonDir, dir);
|
|
43
|
+
if (!exists(d)) continue;
|
|
44
|
+
for (const f of listYamlFiles(d)) out.push({ filename: f, type: label });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// deploy/<target> — 선택된 타겟 외 폴더는 전부 고아 후보
|
|
48
|
+
const deployRoot = join(commonDir, "deploy");
|
|
49
|
+
if (exists(deployRoot)) {
|
|
50
|
+
for (const e of readdirSync(deployRoot, { withFileTypes: true })) {
|
|
51
|
+
if (!e.isDirectory() || e.name === deployTarget) continue;
|
|
52
|
+
for (const f of listYamlFiles(join(deployRoot, e.name))) out.push({ filename: f, type: `deploy/${e.name}` });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 선택 안 된 타입·옵션의 템플릿 워크플로우가 대상 레포에 실재하면 고아로 반환.
|
|
59
|
+
export function detectOrphanWorkflows({ tempDir, targetRoot = ".", selectedTypes = [], options = null }) {
|
|
30
60
|
const projectTypesDir = join(tempDir, PATHS.workflowsDir, PATHS.projectTypesDir);
|
|
31
61
|
if (!exists(projectTypesDir)) return [];
|
|
32
62
|
const selected = new Set(selectedTypes);
|
|
@@ -42,6 +72,14 @@ export function detectOrphanWorkflows({ tempDir, targetRoot = ".", selectedTypes
|
|
|
42
72
|
if (existsSync(join(workflowsDir, f))) orphans.push({ filename: f, type: e.name });
|
|
43
73
|
}
|
|
44
74
|
}
|
|
75
|
+
// common의 조건부 폴더 — options를 준 호출부에서만 검사한다(구 호출부 동작 보존).
|
|
76
|
+
if (options) {
|
|
77
|
+
const commonDir = join(projectTypesDir, "common");
|
|
78
|
+
for (const o of commonOptionalOrphans(commonDir, options)) {
|
|
79
|
+
if (keep.has(o.filename)) continue;
|
|
80
|
+
if (existsSync(join(workflowsDir, o.filename))) orphans.push(o);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
45
83
|
return orphans.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
46
84
|
}
|
|
47
85
|
|
package/src/core/version-yml.js
CHANGED
|
@@ -56,7 +56,7 @@ const HEADER = `# ==============================================================
|
|
|
56
56
|
// (options-ask.js가 이 함수를 import한다 — 순환 방지 위해 여기(version-yml)에 정의.)
|
|
57
57
|
export function parseTemplateOptions(content) {
|
|
58
58
|
const out = { deploy: null, publish: null, secretBackup: null,
|
|
59
|
-
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null,
|
|
59
|
+
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null, aiPrSummary: null,
|
|
60
60
|
deployBranch: null, intent: null, semverAuto: null, appRelease: null };
|
|
61
61
|
// deploy_branch는 metadata 직속(#456) — template.options 밖이라 별도로 스캔한다.
|
|
62
62
|
for (const line of String(content || "").split("\n")) {
|
|
@@ -83,6 +83,10 @@ export function parseTemplateOptions(content) {
|
|
|
83
83
|
if (inCodeReview) {
|
|
84
84
|
const cm = line.match(/^\s+coderabbit:\s*(.+)/);
|
|
85
85
|
if (cm) { const v = strip(cm[1]); if (v === "true") out.codeReviewCoderabbit = true; if (v === "false") out.codeReviewCoderabbit = false; continue; }
|
|
86
|
+
// #566 — AI 변경 요약 워크플로우 포함 여부. 키가 없으면 null(미설정)로 두어
|
|
87
|
+
// 기존 저장소가 업데이트할 때 마법사가 한 번 물어볼 수 있게 한다.
|
|
88
|
+
const am = line.match(/^\s+ai_summary:\s*(.+)/);
|
|
89
|
+
if (am) { const v = strip(am[1]); if (v === "true") out.aiPrSummary = true; if (v === "false") out.aiPrSummary = false; continue; }
|
|
86
90
|
}
|
|
87
91
|
if (inChangelog) {
|
|
88
92
|
const pm = line.match(/^\s+provider:\s*(.+)/);
|
|
@@ -313,7 +317,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
313
317
|
// template 옵션 블록 (.sh save_template_options 신규 추가 케이스). templateOptions 지정 시.
|
|
314
318
|
if (templateOptions) {
|
|
315
319
|
const { templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, optionsDate = today,
|
|
316
|
-
changelogProvider = "
|
|
320
|
+
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true, aiPrSummary = true, intent = null, mode = null,
|
|
317
321
|
semverAuto = true, appRelease = null } = templateOptions;
|
|
318
322
|
const publishJson = `[${publishTargets.map((t) => `"${t}"`).join(",")}]`;
|
|
319
323
|
// intent(프로젝트 성격, #485) — 미지정이면 deploy/publish에서 역추론해 기록 (재통합 시 진입 질문 생략용)
|
|
@@ -338,6 +342,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
338
342
|
}
|
|
339
343
|
out += ` code_review:\n`;
|
|
340
344
|
out += ` coderabbit: ${codeReviewCoderabbit}\n`;
|
|
345
|
+
out += ` ai_summary: ${aiPrSummary}\n`;
|
|
341
346
|
out += ` changelog:\n`;
|
|
342
347
|
out += ` provider: "${changelogProvider}"\n`;
|
|
343
348
|
out += ` base_url: "${changelogBaseUrl}"\n`;
|
package/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { detectOrphanWorkflows } from "./core/orphan-workflows.js";
|
|
|
17
17
|
import { createRunTrace, MIGRATION_DIR } from "./core/run-trace.js";
|
|
18
18
|
import { appendGuideEntry } from "./core/migration-guide.js";
|
|
19
19
|
import { resolveProjectPaths, markerForType } from "./core/paths-resolve.js";
|
|
20
|
-
import { applicableTargets } from "./core/options-ask.js";
|
|
20
|
+
import { applicableTargets, migrateProvider } from "./core/options-ask.js";
|
|
21
21
|
import { printBannerCompact } from "./ui/banner.js";
|
|
22
22
|
import { printSummary } from "./ui/summary.js";
|
|
23
23
|
import { runFull } from "./commands/full.js";
|
|
@@ -197,13 +197,15 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
197
197
|
deployTarget,
|
|
198
198
|
publishTargets,
|
|
199
199
|
includeSecretBackup: opts.includeSecretBackup ?? existing?.options?.secretBackup ?? false,
|
|
200
|
+
// #566 — CLI 값 우선, 없으면 저장값 보존, 그것도 없으면 true(설정 없이 바로 동작).
|
|
201
|
+
aiPrSummary: opts.aiPrSummary ?? existing?.options?.aiPrSummary ?? true,
|
|
200
202
|
// #502 — version 모드가 기존 full 통합 기록(mode)을 강등하지 않도록 (full이 우세)
|
|
201
203
|
recordMode: existing?.templateMode === "full" ? "full" : "version",
|
|
202
204
|
// 릴리스 배포 브랜치(#456): CLI 플래그 → version.yml 저장값 → 빈 값(미출력, 스킬이 develop 폴백)
|
|
203
205
|
deployBranch: opts.deployBranch || existing?.options?.deployBranch || "",
|
|
204
206
|
intent,
|
|
205
207
|
// changelog/code_review 축(#455): 비대화형은 저장값 → 기본값. null이 흘러 provider:"null"로 기록되던 버그 수정.
|
|
206
|
-
changelogProvider: existing?.options?.changelogProvider ?? "
|
|
208
|
+
changelogProvider: migrateProvider(existing?.options?.changelogProvider) ?? "commit",
|
|
207
209
|
changelogBaseUrl: existing?.options?.changelogBaseUrl ?? "",
|
|
208
210
|
codeReviewCoderabbit: existing?.options?.codeReviewCoderabbit ?? false,
|
|
209
211
|
// semver 자동 승격(#546): 저장값 → (기존 통합 레포면 false / 신규면 true).
|
|
@@ -276,7 +278,9 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
276
278
|
// 고아 타입 워크플로우 안내 (#487) — 비대화형은 자동 무해화 금지(배포 파이프라인일 수 있음), 안내만
|
|
277
279
|
if (recordArtifacts) {
|
|
278
280
|
const orphans = trace.step("orphan-scan",
|
|
279
|
-
() => detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types
|
|
281
|
+
() => detectOrphanWorkflows({ tempDir, targetRoot: cwd, selectedTypes: types,
|
|
282
|
+
// 껐는데 남아 계속 도는 워크플로우도 함께 잡는다 (#566)
|
|
283
|
+
options: { includeSecretBackup: context.includeSecretBackup, aiPrSummary: context.aiPrSummary, deployTarget } }),
|
|
280
284
|
{ selectedTypes: types });
|
|
281
285
|
orphanPending = orphans.map((o) => o.filename);
|
|
282
286
|
for (const o of orphans) trace.event("orphan", "detected", o.filename, { type: o.type, action: "안내만(비대화형)" });
|
|
@@ -294,6 +298,39 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
294
298
|
// 알 수 없는 모드 → .sh와 동일하게 복사 0건, 에러 아님
|
|
295
299
|
break;
|
|
296
300
|
}
|
|
301
|
+
// 마이그레이션 기록 (#493/#494) — Layer 2/3 트레이스 파일 + Layer 1 가이드 엔트리
|
|
302
|
+
let migrationGuidePath = null;
|
|
303
|
+
// 기록 파일 경로는 먼저 계산하고(가이드가 참조), 실제 쓰기는 완료 화면 출력 뒤로 미룬다 —
|
|
304
|
+
// 그래야 터미널 미러에 완료 화면까지 담긴다 (#561).
|
|
305
|
+
const files = recordArtifacts
|
|
306
|
+
? trace.paths({ fromVersion: existing?.templateVersion || "", toVersion: context.templateVersion, now })
|
|
307
|
+
: null;
|
|
308
|
+
if (recordArtifacts) {
|
|
309
|
+
migrationGuidePath = appendGuideEntry(cwd, {
|
|
310
|
+
now, mode: opts.mode, types, repoName,
|
|
311
|
+
templateFrom: existing?.templateVersion || "", templateTo: context.templateVersion,
|
|
312
|
+
options: { deploy: deployTarget, publish: publishTargets, secretBackup: context.includeSecretBackup, coderabbit: context.codeReviewCoderabbit, changelogProvider: context.changelogProvider, intent, semverAuto: context.semverAuto , appRelease: context.appRelease },
|
|
313
|
+
branches: { defaultBranch: branch, deployBranch: context.deployBranch || "develop", ready: null, created: null },
|
|
314
|
+
breaking: breakingReport, migrations: migrationsResult, orphans: { cleaned: [], pending: orphanPending },
|
|
315
|
+
events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
|
|
316
|
+
traceFile: files?.traceFile ?? "", logFile: files?.logFile ?? "",
|
|
317
|
+
}).guidePath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
|
|
321
|
+
printSummary({
|
|
322
|
+
mode: opts.mode, types, version, deployBranch: context.deployBranch, migrationGuidePath,
|
|
323
|
+
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
324
|
+
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
325
|
+
logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
|
|
326
|
+
logFile: files?.logFile ?? null,
|
|
327
|
+
traceFile: files?.traceFile ?? null,
|
|
328
|
+
}, cwd);
|
|
329
|
+
|
|
330
|
+
trace.event("run", "end", opts.mode || "", {
|
|
331
|
+
workflowsCopied: result?.workflows?.copied ?? 0,
|
|
332
|
+
workflowsSkipped: result?.workflows?.skipped ?? 0,
|
|
333
|
+
});
|
|
297
334
|
} catch (err) {
|
|
298
335
|
// 실패 원인을 로그에 남긴다 (#561) — 서버 로그처럼 사후에 바로 짚을 수 있어야 한다.
|
|
299
336
|
trace.event("run", "error", opts.mode || "", {
|
|
@@ -302,53 +339,16 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
302
339
|
});
|
|
303
340
|
throw err;
|
|
304
341
|
} finally {
|
|
305
|
-
//
|
|
306
|
-
//
|
|
342
|
+
// 어떤 경로로 빠져나가든 기록을 남긴다 (#561) — 조기 return·중간 취소·예외 포함.
|
|
343
|
+
// 완료 화면 출력이 try 안에 있어야 그 화면까지 미러에 담긴 채로 여기서 닫힌다.
|
|
307
344
|
if (recordArtifacts) {
|
|
308
345
|
trace.finalize({ targetRoot: cwd, fromVersion: existing?.templateVersion || "", toVersion: context.templateVersion, now });
|
|
346
|
+
} else {
|
|
347
|
+
trace.mirrorStop();
|
|
309
348
|
}
|
|
310
349
|
disarmSignals();
|
|
311
350
|
remove(tempDir);
|
|
312
351
|
}
|
|
313
352
|
|
|
314
|
-
// 마이그레이션 기록 (#493/#494) — Layer 2/3 트레이스 파일 + Layer 1 가이드 엔트리
|
|
315
|
-
let migrationGuidePath = null;
|
|
316
|
-
// 기록 파일 경로는 먼저 계산하고(가이드가 참조), 실제 쓰기는 완료 화면 출력 뒤로 미룬다 —
|
|
317
|
-
// 그래야 터미널 미러에 완료 화면까지 담긴다 (#561).
|
|
318
|
-
const files = recordArtifacts
|
|
319
|
-
? trace.paths({ fromVersion: existing?.templateVersion || "", toVersion: context.templateVersion, now })
|
|
320
|
-
: null;
|
|
321
|
-
if (recordArtifacts) {
|
|
322
|
-
migrationGuidePath = appendGuideEntry(cwd, {
|
|
323
|
-
now, mode: opts.mode, types, repoName,
|
|
324
|
-
templateFrom: existing?.templateVersion || "", templateTo: context.templateVersion,
|
|
325
|
-
options: { deploy: deployTarget, publish: publishTargets, secretBackup: context.includeSecretBackup, coderabbit: context.codeReviewCoderabbit, changelogProvider: context.changelogProvider, intent, semverAuto: context.semverAuto , appRelease: context.appRelease },
|
|
326
|
-
branches: { defaultBranch: branch, deployBranch: context.deployBranch || "develop", ready: null, created: null },
|
|
327
|
-
breaking: breakingReport, migrations: migrationsResult, orphans: { cleaned: [], pending: orphanPending },
|
|
328
|
-
events: trace.events, counters: { skipped: result?.workflows?.skipped ?? 0 },
|
|
329
|
-
traceFile: files?.traceFile ?? "", logFile: files?.logFile ?? "",
|
|
330
|
-
}).guidePath;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
|
|
334
|
-
printSummary({
|
|
335
|
-
mode: opts.mode, types, version, deployBranch: context.deployBranch, migrationGuidePath,
|
|
336
|
-
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
337
|
-
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
338
|
-
logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
|
|
339
|
-
logFile: files?.logFile ?? null,
|
|
340
|
-
traceFile: files?.traceFile ?? null,
|
|
341
|
-
}, cwd);
|
|
342
|
-
|
|
343
|
-
// 완료 화면까지 캡처한 뒤 종료하고 기록한다 (#561)
|
|
344
|
-
trace.event("run", "end", opts.mode || "", {
|
|
345
|
-
workflowsCopied: result?.workflows?.copied ?? 0,
|
|
346
|
-
workflowsSkipped: result?.workflows?.skipped ?? 0,
|
|
347
|
-
});
|
|
348
|
-
if (recordArtifacts) {
|
|
349
|
-
trace.finalize({ targetRoot: cwd, fromVersion: existing?.templateVersion || "", toVersion: context.templateVersion, now });
|
|
350
|
-
} else {
|
|
351
|
-
trace.mirrorStop();
|
|
352
|
-
}
|
|
353
353
|
return 0;
|
|
354
354
|
}
|
package/src/ui/summary.js
CHANGED
|
@@ -131,23 +131,6 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
131
131
|
err(" 📚 워크플로우 가이드: .github/workflows/project-types/README.md");
|
|
132
132
|
// #493 — 이번 실행의 마이그레이션 기록. "뭐가 남았고 AI에게 어떻게 시키는지"가 바로 보이게 행동 유도형으로 안내.
|
|
133
133
|
// 실행 기록 위치 (#561) — 무슨 일이 있었는지 나중에 확인할 자리를 알린다.
|
|
134
|
-
// 폴더가 아니라 이번 실행의 파일을 정확히 짚어준다 — 실행이 쌓이면 어느 것이 이번 건인지
|
|
135
|
-
// 모른다. 붙여넣어 바로 열 수 있는 경로가 목적이다.
|
|
136
|
-
if (ctx?.logFile || ctx?.logDir) {
|
|
137
|
-
err(` 📁 실행 로그: ${ctx.logFile || `${ctx.logDir}/`}`);
|
|
138
|
-
if (ctx.traceFile) err(` 이벤트(JSONL): ${ctx.traceFile}`);
|
|
139
|
-
err(" 이번 실행에서 무엇을 어떤 근거로 정했는지 전부 기록돼 있습니다.");
|
|
140
|
-
err(" 문제가 생기면 이 파일을 확인하세요 (저장소에 추적되지 않습니다).");
|
|
141
|
-
err(" 💡 AI Agent에게 \"실행 로그 확인해줘\"라고 요청하면 원인을 짚어줍니다.");
|
|
142
|
-
err("");
|
|
143
|
-
}
|
|
144
|
-
if (ctx?.migrationGuidePath) {
|
|
145
|
-
err(` 🧭 마이그레이션 가이드: ${ctx.migrationGuidePath}`);
|
|
146
|
-
err(" 이번 설치에서 바뀐 내용과 직접 확인해야 할 작업이 이 파일에 정리되어 있습니다.");
|
|
147
|
-
err(" 💡 AI Agent(Claude, Cursor 등)에게 \"마이그레이션 가이드 확인하고");
|
|
148
|
-
err(" 남은 작업 처리해줘\"라고 요청하면 이 문서를 읽고 대신 진행합니다.");
|
|
149
|
-
}
|
|
150
|
-
err("");
|
|
151
134
|
|
|
152
135
|
// 설치 후 검증 결과 (#549) — 문제가 있을 때만 펼치고, 정상이면 한 줄로 압축한다.
|
|
153
136
|
const verification = ctx?.verification;
|
|
@@ -215,4 +198,17 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
215
198
|
err(`${CYAN}📖 자세한 설정 방법은 다음 파일을 참고하세요:${NC}`);
|
|
216
199
|
err(" → PROJECTOPS-SETUP-GUIDE.md");
|
|
217
200
|
err("");
|
|
201
|
+
// 기록 안내는 완료 화면의 맨 끝에 둔다 (#561) — Secret 목록이 길면 위쪽은 스크롤에 묻혀
|
|
202
|
+
// 사용자가 "로그 얘기가 없다"고 느낀다. 문제가 생겼을 때 가장 먼저 찾는 것이므로 마지막 자리다.
|
|
203
|
+
if (ctx?.logFile || ctx?.logDir || ctx?.migrationGuidePath) {
|
|
204
|
+
err(`${CYAN}📁 이번 실행 기록${NC}`);
|
|
205
|
+
if (ctx?.logFile || ctx?.logDir) {
|
|
206
|
+
err(` 로그: ${ctx.logFile || `${ctx.logDir}/`}`);
|
|
207
|
+
if (ctx.traceFile) err(` 이벤트: ${ctx.traceFile}`);
|
|
208
|
+
}
|
|
209
|
+
if (ctx?.migrationGuidePath) err(` 가이드: ${ctx.migrationGuidePath}`);
|
|
210
|
+
err(" 무엇을 어떤 근거로 정했는지 전부 남아 있습니다 (저장소에 추적되지 않음).");
|
|
211
|
+
err(" 💡 문제가 생기면 AI Agent에게 \"실행 로그 확인해줘\"라고 요청하세요.");
|
|
212
|
+
err("");
|
|
213
|
+
}
|
|
218
214
|
}
|