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.
- package/README.md +15 -4
- package/package.json +1 -1
- package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
- package/payload/scripts/issue_helper.py +334 -0
- package/payload/scripts/truncate_release_notes.py +89 -0
- package/payload/version.yml.template +1 -0
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +47 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +4 -4
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +1 -1
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +1 -1
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +1 -1
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +6 -6
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +1 -1
- package/payload/workflows/spring/{PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml → nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml} +8 -2
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +8 -6
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +2 -2
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +8 -8
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +2 -2
- package/src/cli/args.js +9 -0
- package/src/cli/help.js +2 -1
- package/src/commands/dry-run.js +7 -16
- package/src/commands/full.js +88 -14
- package/src/commands/interactive.js +73 -12
- package/src/commands/purge.js +2 -0
- package/src/commands/status.js +21 -1
- package/src/commands/uninstall.js +6 -1
- package/src/core/baseline.js +76 -0
- package/src/core/copy/simple.js +2 -2
- package/src/core/copy/workflows.js +178 -90
- package/src/core/deploy-style.js +90 -0
- package/src/core/detect-fs.js +36 -7
- package/src/core/detect.js +68 -3
- package/src/core/install-log.js +182 -0
- package/src/core/options-ask.js +5 -2
- package/src/core/paths-resolve.js +4 -8
- package/src/core/removal-plan.js +6 -2
- package/src/core/verify.js +86 -0
- package/src/core/version-yml.js +31 -4
- package/src/core/wizard-env.js +6 -3
- package/src/index.js +15 -2
- package/src/ui/env-plan.js +63 -33
- package/src/ui/prompts.js +41 -2
- package/src/ui/status-cards.js +7 -3
- 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
|
-
|
|
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
|
}
|
package/src/ui/env-plan.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 });
|
package/src/ui/status-cards.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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("
|
|
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
|
-
|
|
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(
|
|
164
|
+
err(` ${num()} GitHub Actions 권한 확인`);
|
|
115
165
|
err(" → Settings > Actions > Workflow permissions: Read and write");
|
|
116
166
|
err("");
|
|
117
167
|
err(SEPARATOR);
|