project-auto-wizard 0.1.34 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +15 -4
  2. package/package.json +1 -1
  3. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  4. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  5. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
  6. package/payload/scripts/issue_helper.py +334 -0
  7. package/payload/scripts/truncate_release_notes.py +89 -0
  8. package/payload/version.yml.template +1 -0
  9. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +47 -0
  10. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +4 -4
  11. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +1 -1
  12. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +1 -1
  13. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +1 -1
  14. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +6 -6
  15. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +1 -1
  16. package/payload/workflows/spring/{PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml → nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml} +8 -2
  17. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +8 -6
  18. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +2 -2
  19. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +8 -8
  20. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +2 -2
  21. package/src/cli/args.js +9 -0
  22. package/src/cli/help.js +2 -1
  23. package/src/commands/dry-run.js +7 -16
  24. package/src/commands/full.js +88 -14
  25. package/src/commands/interactive.js +73 -12
  26. package/src/commands/purge.js +2 -0
  27. package/src/commands/status.js +21 -1
  28. package/src/commands/uninstall.js +6 -1
  29. package/src/core/baseline.js +76 -0
  30. package/src/core/copy/simple.js +2 -2
  31. package/src/core/copy/workflows.js +178 -90
  32. package/src/core/deploy-style.js +90 -0
  33. package/src/core/detect-fs.js +36 -7
  34. package/src/core/detect.js +68 -3
  35. package/src/core/install-log.js +182 -0
  36. package/src/core/options-ask.js +5 -2
  37. package/src/core/paths-resolve.js +4 -8
  38. package/src/core/removal-plan.js +6 -2
  39. package/src/core/verify.js +86 -0
  40. package/src/core/version-yml.js +31 -4
  41. package/src/core/wizard-env.js +6 -3
  42. package/src/index.js +15 -2
  43. package/src/ui/env-plan.js +63 -33
  44. package/src/ui/prompts.js +41 -2
  45. package/src/ui/status-cards.js +7 -3
  46. package/src/ui/summary.js +56 -6
package/src/index.js CHANGED
@@ -8,8 +8,9 @@ import { createInterface } from "node:readline/promises";
8
8
  import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
9
9
  import { HELP_TEXT } from "./cli/help.js";
10
10
  import { createContext } from "./context.js";
11
+ import { DEFAULT_DEPLOY_STYLE, isDeployStyle } from "./core/deploy-style.js";
11
12
  import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
12
- import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber } from "./core/detect-fs.js";
13
+ import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber, detectMarkers } from "./core/detect-fs.js";
13
14
  import { parseExisting } from "./core/version-yml.js";
14
15
  import { runBreakingCheck } from "./core/breaking-check.js";
15
16
  import { resolveProjectPaths } from "./core/paths-resolve.js";
@@ -216,7 +217,10 @@ export async function run(argv, {
216
217
  // 감지 (CLI 인자 우선, 없으면 자동 감지 — version.yml 우선 규칙은 detectTypes/detectVersion 내부)
217
218
  const types = opts.types.length ? opts.types : detectTypes(cwd);
218
219
  // version: 기존 version.yml 최우선(SSoT — 재실행 시 덮어쓰기 방지) → CLI 지정 → 파일 감지
219
- const version = (existing?.version) || opts.version || detectVersion(cwd);
220
+ // 비대화형이므로 폴백 안내는 CLI 문구(--project-version) 그대로 쓴다 (이슈 #80).
221
+ const detectWarnings = [];
222
+ const version = (existing?.version) || opts.version
223
+ || detectVersion(cwd, { warn: (m) => { detectWarnings.push(m); console.error(m); } });
220
224
  const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지 (.sh L2208~2221, 이슈 #41)
221
225
  const branch = detectDefaultBranch(cwd);
222
226
  const repoName = detectRepoName(cwd);
@@ -267,6 +271,11 @@ export async function run(argv, {
267
271
  // 실 resolver 4종 (.sh resolve_token 등가)
268
272
  resolvers: makeResolvers(cwd, repoName, paths),
269
273
  now, today,
274
+ // 설치 로그(#79)용 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
275
+ markers: detectMarkers(cwd, types), detectWarnings,
276
+ deployStyle: opts.deployStyle
277
+ || (isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE),
278
+ previousTemplateVersion: existing?.templateVersion || "",
270
279
  });
271
280
 
272
281
  context.templateVersion = readTemplateVersion();
@@ -293,6 +302,10 @@ export async function run(argv, {
293
302
  mode: opts.mode, types, version, versionCode, branches,
294
303
  copiedFiles: result?.workflows?.copiedFiles ?? [],
295
304
  gitignoreUpdated: result?.gitignoreUpdated === true,
305
+ unresolved: result?.unresolved ?? [],
306
+ secrets: result?.secrets ?? new Map(),
307
+ installLogPath: result?.installLog?.path ?? "",
308
+ cleanup: result?.cleanup ?? null,
296
309
  });
297
310
  return 0;
298
311
  }
@@ -10,6 +10,7 @@ import { PAYLOAD } from "../core/paths.js";
10
10
  import { exists, listYamlFiles } from "../core/fsutil.js";
11
11
  import { parseWizardLine, resolveToken } from "../core/wizard-env.js";
12
12
  import { loadWizardPrompts, wfField, workflowDisplayName } from "../core/wizard-labels.js";
13
+ import { deployFilter } from "../core/deploy-style.js";
13
14
  import * as engine from "./readline-engine.js";
14
15
 
15
16
  const CANCEL = engine.CANCEL;
@@ -38,40 +39,47 @@ export function scopeString(usages = []) {
38
39
  // 반환: { keys:[], defaults:Map<key,default>, typeDefaults:Map<"type|key",default>,
39
40
  // usages:Map<key,[{type,workflowName}]> }
40
41
  export function collectAsks(payloadRoot, types = [], opts = {}) {
41
- const { resolvers = {}, includeNexus = false, prompts = null } = opts;
42
+ const { resolvers = {}, includeNexus = false, includeSecretBackup = false, deployStyle = "", prompts = null } = opts;
43
+ // 설치하지 않을 배포 워크플로우의 질문까지 묻지 않는다 — 질문 수는 설치 범위를 따라간다.
44
+ const keepDeploy = deployFilter(deployStyle);
42
45
  const baseDir = join(payloadRoot, PAYLOAD.workflowsDir);
43
46
  const keys = [];
44
47
  const defaults = new Map();
45
48
  const typeDefaults = new Map();
46
49
  const usages = new Map();
47
50
 
51
+ // 스캔 단위: [타입, 폴더]. secret-backup은 타입이 아니라 공통이지만 @wizard 마커를 가지므로
52
+ // 포함하기로 한 경우에만 질문 수집 대상이 된다 (이슈 #82) — 종전에는 스캔 대상이 아니어서
53
+ // my-project 같은 예시값이 질문 없이 그대로 설치됐다.
54
+ const units = [];
48
55
  for (const type of types) {
49
56
  const typeDir = join(baseDir, type);
50
57
  if (!exists(typeDir)) continue;
51
58
  // 복사 엔진과 동일한 폴더 구성: 타입 직하위 + (nexus 아니면) server-deploy + (nexus면) nexus
52
- const dirs = [typeDir];
53
- if (!includeNexus) dirs.push(join(typeDir, "server-deploy"));
54
- else dirs.push(join(typeDir, "nexus"));
55
-
56
- for (const dir of dirs) {
57
- if (!exists(dir)) continue;
58
- for (const filename of listYamlFiles(dir)) {
59
- const content = readFileSync(join(dir, filename), "utf8");
60
- if (!content.includes("@wizard")) continue;
61
- const workflowName = workflowDisplayName(prompts, filename);
62
- for (const line of content.split(/\r?\n/)) {
63
- const p = parseWizardLine(line); // KEY 정규식 [A-Z_]+ (.sh와 동일)
64
- if (!p || p.action !== "ask") continue;
65
- // 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴 (.sh _type_default 등가)
66
- const typeDefault = p.arg.startsWith("@")
67
- ? resolveToken(p.arg.slice(1), type, resolvers)
68
- : p.arg;
69
- typeDefaults.set(`${type}|${p.key}`, typeDefault);
70
- if (!defaults.has(p.key)) { keys.push(p.key); defaults.set(p.key, typeDefault); }
71
- const list = usages.get(p.key) || [];
72
- list.push({ type, workflowName });
73
- usages.set(p.key, list);
74
- }
59
+ units.push([type, typeDir, null]);
60
+ units.push([type, join(typeDir, includeNexus ? "nexus" : "server-deploy"), includeNexus ? null : keepDeploy]);
61
+ }
62
+ if (includeSecretBackup) units.push(["common", join(baseDir, "common", "secret-backup"), null]);
63
+
64
+ for (const [type, dir, fileFilter] of units) {
65
+ if (!exists(dir)) continue;
66
+ for (const filename of listYamlFiles(dir)) {
67
+ if (fileFilter && !fileFilter(filename)) continue;
68
+ const content = readFileSync(join(dir, filename), "utf8");
69
+ if (!content.includes("@wizard")) continue;
70
+ const workflowName = workflowDisplayName(prompts, filename);
71
+ for (const line of content.split(/\r?\n/)) {
72
+ const p = parseWizardLine(line); // KEY 정규식 [A-Z_]+ (.sh 동일)
73
+ if (!p || p.action !== "ask") continue;
74
+ // 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴 (.sh _type_default 등가)
75
+ const typeDefault = p.arg.startsWith("@")
76
+ ? resolveToken(p.arg.slice(1), type, resolvers)
77
+ : p.arg;
78
+ typeDefaults.set(`${type}|${p.key}`, typeDefault);
79
+ if (!defaults.has(p.key)) { keys.push(p.key); defaults.set(p.key, typeDefault); }
80
+ const list = usages.get(p.key) || [];
81
+ list.push({ type, workflowName });
82
+ usages.set(p.key, list);
75
83
  }
76
84
  }
77
85
  }
@@ -83,6 +91,23 @@ function firstTypeFor(usages, key) {
83
91
  return usages.get(key)?.[0]?.type ?? "";
84
92
  }
85
93
 
94
+ // 최종 답변 목록 (이슈 #79, #80) — 완료 요약과 설치 로그가 같은 데이터를 쓰도록 여기서 만든다.
95
+ // isDefault는 "기본값 그대로인가"다. 나중에 배포가 안 될 때 제일 먼저 확인하게 되는 정보라
96
+ // 값만 남기면 부족하다.
97
+ function buildAnswers(prompts, asks, values, useDefaults) {
98
+ return asks.keys.map((key) => {
99
+ const def = asks.defaults.get(key) ?? "";
100
+ const chosen = useDefaults ? def : (values.get(key) ?? def);
101
+ return {
102
+ key,
103
+ label: wfField(prompts, firstTypeFor(asks.usages, key), key, "label") || key,
104
+ value: chosen,
105
+ isDefault: chosen === def,
106
+ scope: scopeString(asks.usages.get(key) || []),
107
+ };
108
+ });
109
+ }
110
+
86
111
  // KEY 1개를 'label·사용처·설명·예시·기본값' 카드로 출력 (.sh _wf_print_field_card 등가).
87
112
  // info: { default, usages } — idx/tot 있으면 "(i/t)" 진행 표시. log 주입 가능(테스트 무음화).
88
113
  export function printFieldCard(prompts, key, info, idx = null, tot = null, log = defaultLog) {
@@ -124,7 +149,7 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
124
149
  }
125
150
 
126
151
  // 배포 env 설정 계획 (.sh wf_prompt_env_plan 등가).
127
- // 반환: { values: Map<key,value>, useDefaults: boolean }
152
+ // 반환: { values: Map<key,value>, useDefaults: boolean, answers: [{key,label,value,isDefault,scope}] }
128
153
  // - useDefaults=true → 호출부는 substituteEnv에 그대로 넘기면 타입별 기본값 경로(.sh _wf_prefill_all 등가)
129
154
  // - useDefaults=false → values에 담긴 키만 사용자 확정값으로 치환, 나머지는 기본값
130
155
  // (⚠️ substituteEnv는 useDefaults=false일 때만 values를 참조하므로 이 플래그를 반드시 함께 전달)
@@ -136,19 +161,22 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
136
161
  // log — 카드·안내 출력 함수 주입 (기본 stderr)
137
162
  export async function promptEnvPlan({
138
163
  payloadRoot, types = [], io = null, force = false, resolvers = {},
139
- includeNexus = false, targetRoot = ".", repoName = "", log = defaultLog,
164
+ includeNexus = false, includeSecretBackup = false, deployStyle = "", targetRoot = ".", repoName = "", log = defaultLog,
140
165
  } = {}) {
141
166
  const prompts = loadWizardPrompts(targetRoot, payloadRoot);
142
- const asks = collectAsks(payloadRoot, types, { resolvers, includeNexus, prompts });
167
+ const asks = collectAsks(payloadRoot, types, { resolvers, includeNexus, includeSecretBackup, deployStyle, prompts });
143
168
  const defaults = asks.defaults;
144
169
 
145
170
  // 수집 키 0개 → 질문 자체가 없음 (.sh `[ ${#WF_ASK_KEYS[@]} -eq 0 ]` 등가)
146
- if (asks.keys.length === 0) return { values: new Map(), useDefaults: true };
171
+ if (asks.keys.length === 0) return { values: new Map(), useDefaults: true, answers: [] };
147
172
 
148
173
  // 비대화형: force 또는 (io 미주입 && 비TTY) → 전부 기본값 (.sh FORCE_MODE/TTY_AVAILABLE 분기 등가)
149
174
  // io가 주입돼 있으면(테스트/상위 마법사) TTY 여부와 무관하게 대화형으로 진행한다.
150
175
  const interactive = !force && (io != null || stdin.isTTY);
151
- if (!interactive) return { values: new Map(defaults), useDefaults: true };
176
+ if (!interactive) {
177
+ const values = new Map(defaults);
178
+ return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
179
+ }
152
180
 
153
181
  const ui = io ?? engine;
154
182
 
@@ -175,7 +203,8 @@ export async function promptEnvPlan({
175
203
  });
176
204
  // ESC/취소 → 전부 기본값 (.sh `if [ "$_rc" -ne 0 ]` 등가)
177
205
  if (choice === CANCEL || choice == null || choice === "all") {
178
- return { values: new Map(defaults), useDefaults: true };
206
+ const values = new Map(defaults);
207
+ return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
179
208
  }
180
209
 
181
210
  // 사용자가 확정한 키만 values에 담는다 — substituteEnv(useDefaults:false)가
@@ -183,7 +212,7 @@ export async function promptEnvPlan({
183
212
  const values = new Map();
184
213
  if (choice === "each") {
185
214
  await promptEach(ui, prompts, asks, asks.keys, values, log);
186
- return { values, useDefaults: false };
215
+ return { values, useDefaults: false, answers: buildAnswers(prompts, asks, values, false) };
187
216
  }
188
217
 
189
218
  // some: 바꿀 항목만 멀티선택 → 고른 것만 입력 (.sh 3266~3277)
@@ -198,10 +227,11 @@ export async function promptEnvPlan({
198
227
  });
199
228
  // ESC/빈 선택 → 전부 기본값 (.sh: _wf_prefill_all만 수행)
200
229
  if (selected === CANCEL || !Array.isArray(selected) || selected.length === 0) {
201
- return { values: new Map(defaults), useDefaults: true };
230
+ const values = new Map(defaults);
231
+ return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
202
232
  }
203
233
  // 수집 키 순서 유지 + WF_ASK_KEYS 멤버만 인정 (.sh _wf_prefill_interactive 필터 등가)
204
234
  const todo = asks.keys.filter((k) => selected.includes(k));
205
235
  await promptEach(ui, prompts, asks, todo, values, log);
206
- return { values, useDefaults: false };
236
+ return { values, useDefaults: false, answers: buildAnswers(prompts, asks, values, false) };
207
237
  }
package/src/ui/prompts.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // node:readline 기반 자체 엔진 사용 (@clack/prompts 는 Windows TTY에서 Enter가 멈추는 버그로 제거).
3
3
  // 취소(ESC/Ctrl+C)는 각 함수가 CANCEL 심볼을 반환 → 호출부가 정상 종료(exit 0) 처리.
4
4
  import * as engine from "./readline-engine.js";
5
+ import { DEPLOY_STYLES } from "../core/deploy-style.js";
5
6
 
6
7
  export const CANCEL = engine.CANCEL;
7
8
 
@@ -47,17 +48,55 @@ export async function editMenu({ showOptional = false } = {}) {
47
48
  return engine.select({ message: "어떤 항목을 수정할까요?", options });
48
49
  }
49
50
 
51
+ const ALL_TYPES = ["spring", "flutter", "next", "react", "react-native", "react-native-expo", "node", "python", "basic"];
52
+
50
53
  // 타입 멀티선택.
51
54
  export async function selectTypes(current = []) {
52
- const all = ["spring", "flutter", "next", "react", "react-native", "react-native-expo", "node", "python", "basic"];
53
55
  return engine.multiselect({
54
56
  message: "프로젝트 타입을 선택하세요 (Space 토글, Enter 확정)",
55
- options: all.map((t) => ({ value: t, label: t })),
57
+ options: ALL_TYPES.map((t) => ({ value: t, label: t })),
56
58
  initialValues: current.length ? current : ["basic"],
57
59
  required: true,
58
60
  });
59
61
  }
60
62
 
63
+ // 감지 직후 타입 확정 (이슈 #78). selectTypes와 달리 감지 근거 파일을 라벨에 붙여
64
+ // "왜 이렇게 판단했는지"를 보여준다 — 근거가 보여야 맞는지 틀린지 판단할 수 있다.
65
+ // 감지 결과가 맞으면 Enter 한 번으로 끝난다.
66
+ export async function confirmTypes({ types = [], markers = null } = {}) {
67
+ const detected = new Set(types);
68
+ engine.note(
69
+ "선택한 타입에 따라 설치되는 CI/CD 워크플로우와 버전 동기화 대상 파일이 달라집니다.\n" +
70
+ "감지 결과가 맞으면 그대로 Enter를 누르세요.",
71
+ "프로젝트 타입 확정",
72
+ );
73
+ return engine.multiselect({
74
+ message: "이 프로젝트의 타입입니다 (Space 토글, Enter 확정)",
75
+ options: ALL_TYPES.map((t) => {
76
+ const marker = markers?.get?.(t);
77
+ // 감지된 타입만 근거를 붙인다 — 나머지는 후보로만 나열한다.
78
+ return { value: t, label: detected.has(t) && marker ? `${t} — ${marker} 발견` : t };
79
+ }),
80
+ initialValues: types.length ? types : ["basic"],
81
+ required: true,
82
+ });
83
+ }
84
+
85
+ // 배포 방식 선택 (이슈 #80). 서버 배포 CD 워크플로우는 서로 대체재라 하나만 쓴다.
86
+ // 고른 것만 설치하고 push 트리거까지 켜준다 — 종전에는 넷을 다 깔고 SIMPLE만 켜져 있어,
87
+ // 무중단을 원한 사람은 설치 후 YAML을 직접 고쳐야 했다.
88
+ export async function selectDeployStyle() {
89
+ engine.note(
90
+ "서버 배포 워크플로우는 서로 대체재입니다 (Nginx와 Traefik을 동시에 쓰지 않습니다).\n" +
91
+ "고른 방식만 설치하고 자동 실행(push 트리거)까지 켭니다. PR 프리뷰는 선택과 무관하게 함께 설치됩니다.",
92
+ "배포 방식",
93
+ );
94
+ return engine.select({
95
+ message: "서버 배포는 어떤 방식으로 할까요?",
96
+ options: DEPLOY_STYLES.map((s) => ({ value: s.value, label: s.label })),
97
+ });
98
+ }
99
+
61
100
  // 텍스트 입력 (빈 입력=기본값 유지).
62
101
  export async function askText(message, defaultValue = "") {
63
102
  const v = await engine.text({ message, defaultValue });
@@ -2,24 +2,28 @@
2
2
  // (층5의 Breaking Changes 박스는 core/breaking-check.js가 담당.
3
3
  // 원본의 층4 IDE Skills 상태는 project-auto-wizard 스코프 제외 — Agent Skills 미포함)
4
4
  import { A, paint } from "./ansi.js";
5
- import { markerForType } from "../core/detect.js";
6
5
 
7
6
  const GUT = paint("│", A.gray);
8
7
  const HEAD = paint("◆", A.cyan);
9
8
  const OK = paint("✓", A.green);
10
9
 
11
10
  // 층2 — 감지 로그 (.ps1 감지 진행 표시 등가)
12
- export function printDetectionLog({ types = [], version = "", branch = "" }, out = (s) => process.stderr.write(s)) {
11
+ // markers: Map<type, 실제 발견 파일> (이슈 #77).
12
+ // warnings: 감지 도중 나온 경고. 감지 함수를 먼저 호출한 뒤 박스를 그리는 구조라 경고가
13
+ // 박스 위로 새어나가 앞선 질문에 대한 경고처럼 보였다 — 박스 안에서 출력한다 (이슈 #80).
14
+ export function printDetectionLog({ types = [], version = "", branch = "", markers = new Map(), warnings = [] },
15
+ out = (s) => process.stderr.write(s)) {
13
16
  out(`${paint("┌", A.gray)} 🔍 프로젝트를 살펴보는 중...\n`);
14
17
  if (types.length && !(types.length === 1 && types[0] === "basic")) {
15
18
  for (const t of types) {
16
- const marker = markerForType(t);
19
+ const marker = markers.get(t);
17
20
  out(`${GUT} ${OK} ${marker ? `${marker} 발견 → ` : ""}${paint(t, A.bold)} 감지\n`);
18
21
  }
19
22
  } else {
20
23
  out(`${GUT} ${paint("─", A.dim)} 마커 파일 없음 → ${paint("basic", A.bold)} (직접 선택 가능)\n`);
21
24
  }
22
25
  out(`${GUT} ${OK} 버전: ${paint(`v${version}`, A.green)} · 브랜치: ${paint(branch, A.green)}\n`);
26
+ for (const w of warnings) out(`${GUT} ${paint(w, A.yellow)}\n`);
23
27
  out(`${GUT}\n`);
24
28
  }
25
29
 
package/src/ui/summary.js CHANGED
@@ -6,7 +6,9 @@ import { paint, A, colorEnabled } from "./ansi.js";
6
6
  const SEPARATOR = "────────────────────────────────────────";
7
7
 
8
8
  export function printSummary(ctx) {
9
- const { mode, types = [], version = "", versionCode = null, copiedFiles = [], branches = null, gitignoreUpdated = false } = ctx || {};
9
+ const { mode, types = [], version = "", versionCode = null, copiedFiles = [], branches = null, gitignoreUpdated = false,
10
+ // 설치 후 검증·기록 (#79, #80, #81)
11
+ answers = [], unresolved = [], secrets = new Map(), installLogPath = "", cleanup = null } = ctx || {};
10
12
  const err = (s = "") => process.stderr.write(`${s}\n`);
11
13
  // 색상은 ansi.js의 공용 가드로 통일 (NO_COLOR + stderr TTY 여부)
12
14
  const enabled = colorEnabled(process.stderr);
@@ -87,14 +89,38 @@ export function printSummary(ctx) {
87
89
  err("");
88
90
  err(" 🔧 .github/scripts/");
89
91
  err(" ├─ version_manager.py");
90
- err(" └─ changelog_manager.py");
92
+ err(" ├─ changelog_manager.py");
93
+ err(" ├─ truncate_release_notes.py");
94
+ err(" └─ issue_helper.py");
91
95
  err("");
92
96
 
97
+ // 입력한 환경설정 값 (#80) — 마지막으로 눈으로 검산할 기회. 종전에는 답변이 워크플로우
98
+ // YAML 안으로만 사라져, 오타를 내도 배포가 실패한 뒤에야 알 수 있었다.
99
+ if (answers.length) {
100
+ err(" ⚙️ 적용된 환경설정:");
101
+ for (const a of answers) {
102
+ const mark = a.isDefault ? paint(" (기본값)", A.dim, enabled) : "";
103
+ err(` • ${a.label}: ${paint(a.value, A.green, enabled)}${mark}`);
104
+ }
105
+ err("");
106
+ }
107
+ // 배포 방식을 바꿔 재설치한 경우, 이전 CD를 어떻게 처리했는지 알린다 (#80).
108
+ if (cleanup?.removed?.length || cleanup?.backedUp?.length) {
109
+ err(" 🧹 이전 배포 방식 정리:");
110
+ for (const f of cleanup.removed || []) err(` • ${f} ${paint("삭제 (손대지 않은 파일)", A.dim, enabled)}`);
111
+ for (const f of cleanup.backedUp || []) err(` • ${f} → ${f}.bak ${paint("수정하신 내용이 있어 백업", A.dim, enabled)}`);
112
+ err("");
113
+ }
114
+ if (installLogPath) {
115
+ err(` 📋 설치 기록: ${installLogPath}`);
116
+ err(" → 나중에 '무엇을 어떤 값으로 설치했는지' 확인할 때 이 파일을 보세요");
117
+ err("");
118
+ }
119
+
93
120
  // 프로젝트 타입별 안내
94
121
  if (types.includes("spring")) {
95
122
  err(" 💡 Spring 프로젝트 추가 설정:");
96
- err(" • build.gradle의 버전 정보가 자동 동기화됩니다");
97
- err(" • CI/CD 워크플로우에서 GitHub Secrets 설정이 필요합니다");
123
+ err(" • build.gradle / build.gradle.kts / pom.xml 의 버전 정보가 자동 동기화됩니다");
98
124
  err("");
99
125
  }
100
126
 
@@ -106,12 +132,36 @@ export function printSummary(ctx) {
106
132
  err("");
107
133
  err(paint(paint("⚠️ 다음 작업을 확인해주세요:", A.yellow, enabled), A.bold, enabled));
108
134
  err("");
109
- err(" 1️⃣ 릴리스 automerge용 PAT (선택 — 없으면 GITHUB_TOKEN 사용)");
135
+
136
+ let step = 0;
137
+ const num = () => ["1️⃣ ", "2️⃣ ", "3️⃣ ", "4️⃣ ", "5️⃣ "][step++] || " •";
138
+
139
+ // 미치환 플레이스홀더 (#81) — 이 상태로는 해당 워크플로우가 동작하지 않으므로 제일 먼저 알린다.
140
+ if (unresolved.length) {
141
+ err(` ${num()} ${paint("값이 채워지지 않은 항목이 있습니다 — 직접 채워야 동작합니다", A.red, enabled)}`);
142
+ for (const u of unresolved) {
143
+ err(` → ${u.filename}:${u.line} ${paint(u.token, A.bold, enabled)}`);
144
+ }
145
+ err("");
146
+ }
147
+
148
+ // 설치된 워크플로우가 실제로 요구하는 Secret (#80) — 종전에는 하나도 안내되지 않아
149
+ // "설치 성공"인데 배포는 돌지 않는 상태로 끝났다.
150
+ if (secrets.size) {
151
+ err(` ${num()} 아래 GitHub Secret을 등록해야 배포 워크플로우가 동작합니다 (${secrets.size}개)`);
152
+ err(" → Settings > Secrets and variables > Actions");
153
+ for (const [name, users] of secrets) {
154
+ err(` → ${paint(name, A.bold, enabled)} ${paint(users.join(", "), A.dim, enabled)}`);
155
+ }
156
+ err("");
157
+ }
158
+
159
+ err(` ${num()} 릴리스 automerge용 PAT (선택 — 없으면 GITHUB_TOKEN 사용)`);
110
160
  err(" → Repository Settings > Secrets > Actions");
111
161
  err(" → Secret Name: WORKFLOW_PAT (Scopes: repo, workflow)");
112
162
  err(" → GITHUB_TOKEN 머지는 후속 워크플로우를 트리거하지 않습니다");
113
163
  err("");
114
- err(" 2️⃣ GitHub Actions 권한 확인");
164
+ err(` ${num()} GitHub Actions 권한 확인`);
115
165
  err(" → Settings > Actions > Workflow permissions: Read and write");
116
166
  err("");
117
167
  err(SEPARATOR);