project-auto-wizard 0.1.5
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/LICENSE +21 -0
- package/README.md +139 -0
- package/bin/project-auto-wizard.js +19 -0
- package/package.json +36 -0
- package/payload/coderabbit.yaml +19 -0
- package/payload/config/breaking-changes.json +1 -0
- package/payload/config/wizard-prompts.yml +62 -0
- package/payload/scripts/changelog_manager.py +726 -0
- package/payload/scripts/version_manager.py +617 -0
- package/payload/version.yml.template +48 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
- package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
- package/src/cli/args.js +95 -0
- package/src/cli/help.js +27 -0
- package/src/commands/full.js +53 -0
- package/src/commands/interactive.js +276 -0
- package/src/commands/revert.js +60 -0
- package/src/commands/version.js +31 -0
- package/src/commands/workflows.js +49 -0
- package/src/context.js +26 -0
- package/src/core/assets.js +47 -0
- package/src/core/branches.js +60 -0
- package/src/core/branding.js +15 -0
- package/src/core/breaking-check.js +65 -0
- package/src/core/breaking.js +26 -0
- package/src/core/copy/coderabbit.js +26 -0
- package/src/core/copy/gitignore.js +55 -0
- package/src/core/copy/readme.js +30 -0
- package/src/core/copy/simple.js +23 -0
- package/src/core/copy/workflows.js +233 -0
- package/src/core/detect-fs.js +106 -0
- package/src/core/detect.js +62 -0
- package/src/core/fsutil.js +46 -0
- package/src/core/options-ask.js +99 -0
- package/src/core/paths-resolve.js +261 -0
- package/src/core/paths.js +16 -0
- package/src/core/version-yml.js +190 -0
- package/src/core/wizard-env.js +101 -0
- package/src/core/wizard-labels.js +107 -0
- package/src/index.js +162 -0
- package/src/ui/ansi.js +26 -0
- package/src/ui/banner.js +29 -0
- package/src/ui/env-plan.js +207 -0
- package/src/ui/prompts.js +99 -0
- package/src/ui/readline-engine.js +257 -0
- package/src/ui/status-cards.js +56 -0
- package/src/ui/summary.js +127 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// node:readline 기반 대화형 프롬프트 엔진 (@clack/prompts 대체).
|
|
2
|
+
// WHY: @clack/prompts 1.7.0 이 Windows TTY 콘솔에서 Enter(return) 키를 처리하지 못하고
|
|
3
|
+
// 멈추는 버그가 있다(실측 확정). node:readline 의 keypress 이벤트는 Windows에서 정상 동작한다.
|
|
4
|
+
// .sh/.ps1 이 자체 메뉴를 구현한 것과 동일한 접근. 외부 의존성 0 → 내부망에서도 안전.
|
|
5
|
+
//
|
|
6
|
+
// 계약: 취소(ESC/Ctrl+C)는 CANCEL 심볼 반환. 각 함수 async.
|
|
7
|
+
import { emitKeypressEvents } from "node:readline";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
|
|
10
|
+
export const CANCEL = Symbol("cancel");
|
|
11
|
+
|
|
12
|
+
// ── ANSI 헬퍼 (picocolors 대체 — 의존성 0) ───────────────────────────
|
|
13
|
+
const ESC = "\x1b[";
|
|
14
|
+
const c = {
|
|
15
|
+
reset: `${ESC}0m`, dim: `${ESC}2m`, bold: `${ESC}1m`,
|
|
16
|
+
cyan: `${ESC}36m`, green: `${ESC}32m`, gray: `${ESC}90m`, yellow: `${ESC}33m`,
|
|
17
|
+
};
|
|
18
|
+
const paint = (s, color) => `${color}${s}${c.reset}`;
|
|
19
|
+
const hideCursor = () => stdout.write(`${ESC}?25l`);
|
|
20
|
+
const showCursor = () => stdout.write(`${ESC}?25h`);
|
|
21
|
+
|
|
22
|
+
// 심볼 (clack 톤 유지)
|
|
23
|
+
const S_ACTIVE = paint("●", c.green);
|
|
24
|
+
const S_INACTIVE = paint("○", c.dim);
|
|
25
|
+
const S_CHECK_ON = paint("◼", c.green);
|
|
26
|
+
const S_CHECK_OFF = paint("◻", c.dim);
|
|
27
|
+
const S_BAR = paint("│", c.gray);
|
|
28
|
+
const S_Q = paint("◆", c.cyan);
|
|
29
|
+
const S_DONE = paint("◇", c.green);
|
|
30
|
+
|
|
31
|
+
// 여러 줄 지운 뒤 커서를 블록 시작으로 되돌리는 렌더러.
|
|
32
|
+
// prevLines 만큼 위로 올라가 지우고 새로 그린다.
|
|
33
|
+
function makeRenderer() {
|
|
34
|
+
let prevLines = 0;
|
|
35
|
+
return {
|
|
36
|
+
render(lines) {
|
|
37
|
+
if (prevLines > 0) stdout.write(`${ESC}${prevLines}A`); // 위로
|
|
38
|
+
stdout.write(`${ESC}0J`); // 커서 아래 전부 지우기
|
|
39
|
+
stdout.write(lines.join("\n") + "\n");
|
|
40
|
+
prevLines = lines.length;
|
|
41
|
+
},
|
|
42
|
+
reset() { prevLines = 0; },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// raw keypress 세션 공통 래퍼. onKey(str,key) → true 반환 시 종료.
|
|
47
|
+
// 반환값은 finalize()가 만든다. 취소 시 CANCEL.
|
|
48
|
+
function keySession(renderFn, onKey) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
51
|
+
emitKeypressEvents(stdin);
|
|
52
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
53
|
+
stdin.resume();
|
|
54
|
+
hideCursor();
|
|
55
|
+
|
|
56
|
+
const cleanup = () => {
|
|
57
|
+
stdin.removeListener("keypress", handler);
|
|
58
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
59
|
+
stdin.pause();
|
|
60
|
+
showCursor();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handler = (str, key) => {
|
|
64
|
+
key = key || {};
|
|
65
|
+
// 취소: Ctrl+C / ESC
|
|
66
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
67
|
+
cleanup();
|
|
68
|
+
resolve(CANCEL);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const done = onKey(str, key);
|
|
72
|
+
if (done !== undefined) {
|
|
73
|
+
cleanup();
|
|
74
|
+
resolve(done);
|
|
75
|
+
} else {
|
|
76
|
+
renderFn();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
stdin.on("keypress", handler);
|
|
80
|
+
renderFn(); // 최초 렌더
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── 단일 선택 (방향키 + Enter) ───────────────────────────────────────
|
|
85
|
+
// options: [{value,label,hint?}]. 반환: 선택 value 또는 CANCEL.
|
|
86
|
+
export async function select({ message, options, initialIndex = 0 }) {
|
|
87
|
+
if (!stdin.isTTY) {
|
|
88
|
+
// 비-TTY: 기본값(첫 항목) 반환 — 파이프 환경 방어
|
|
89
|
+
return options[initialIndex]?.value;
|
|
90
|
+
}
|
|
91
|
+
const r = makeRenderer();
|
|
92
|
+
let idx = Math.max(0, Math.min(initialIndex, options.length - 1));
|
|
93
|
+
|
|
94
|
+
const draw = () => {
|
|
95
|
+
const lines = [S_BAR, `${S_Q} ${paint(message, c.bold)}`];
|
|
96
|
+
options.forEach((o, i) => {
|
|
97
|
+
const sel = i === idx;
|
|
98
|
+
const marker = sel ? S_ACTIVE : S_INACTIVE;
|
|
99
|
+
const label = sel ? paint(o.label, c.cyan) : o.label;
|
|
100
|
+
const hint = o.hint && sel ? paint(` (${o.hint})`, c.dim) : "";
|
|
101
|
+
lines.push(`${S_BAR} ${marker} ${label}${hint}`);
|
|
102
|
+
});
|
|
103
|
+
lines.push(paint(`└ ↑/↓ 이동 · Enter 확정 · ESC 취소`, c.gray));
|
|
104
|
+
r.render(lines);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = await keySession(draw, (str, key) => {
|
|
108
|
+
if (key.name === "up" || key.name === "k") { idx = (idx - 1 + options.length) % options.length; return; }
|
|
109
|
+
if (key.name === "down" || key.name === "j") { idx = (idx + 1) % options.length; return; }
|
|
110
|
+
// 숫자 점프 (1-9)
|
|
111
|
+
if (/^[1-9]$/.test(str || "")) {
|
|
112
|
+
const n = Number(str) - 1;
|
|
113
|
+
if (n < options.length) { idx = n; return; }
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.name === "return" || key.name === "enter") return options[idx].value;
|
|
117
|
+
return; // 그 외 키: 무시하고 계속
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 확정 화면 다시 그리기 (◇ 완료 심볼 + 선택값)
|
|
121
|
+
if (result !== CANCEL) {
|
|
122
|
+
const chosen = options.find((o) => o.value === result);
|
|
123
|
+
r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(chosen?.label ?? "", c.dim)}`]);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── 다중 선택 (Space 토글 + Enter) ──────────────────────────────────
|
|
129
|
+
// options: [{value,label,hint?,disabled?}]. 반환: 선택 value 배열 또는 CANCEL.
|
|
130
|
+
export async function multiselect({ message, options, initialValues = [], required = false }) {
|
|
131
|
+
if (!stdin.isTTY) {
|
|
132
|
+
return initialValues.length ? [...initialValues] : (required ? [options[0]?.value].filter(Boolean) : []);
|
|
133
|
+
}
|
|
134
|
+
const r = makeRenderer();
|
|
135
|
+
let idx = 0;
|
|
136
|
+
const chosen = new Set(initialValues);
|
|
137
|
+
let warn = "";
|
|
138
|
+
|
|
139
|
+
const draw = () => {
|
|
140
|
+
const lines = [S_BAR, `${S_Q} ${paint(message, c.bold)}`];
|
|
141
|
+
options.forEach((o, i) => {
|
|
142
|
+
const cur = i === idx;
|
|
143
|
+
const box = chosen.has(o.value) ? S_CHECK_ON : S_CHECK_OFF;
|
|
144
|
+
const pointer = cur ? paint("❯", c.cyan) : " ";
|
|
145
|
+
const label = cur ? paint(o.label, c.cyan) : (o.disabled ? paint(o.label, c.dim) : o.label);
|
|
146
|
+
const hint = o.hint ? paint(` (${o.hint})`, c.dim) : "";
|
|
147
|
+
lines.push(`${S_BAR} ${pointer} ${box} ${label}${hint}`);
|
|
148
|
+
});
|
|
149
|
+
if (warn) lines.push(paint(` ${warn}`, c.yellow));
|
|
150
|
+
lines.push(paint(`└ ↑/↓ 이동 · Space 토글 · Enter 확정 · ESC 취소`, c.gray));
|
|
151
|
+
r.render(lines);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = await keySession(draw, (str, key) => {
|
|
155
|
+
warn = "";
|
|
156
|
+
if (key.name === "up" || key.name === "k") { idx = (idx - 1 + options.length) % options.length; return; }
|
|
157
|
+
if (key.name === "down" || key.name === "j") { idx = (idx + 1) % options.length; return; }
|
|
158
|
+
if (key.name === "space" || str === " ") {
|
|
159
|
+
const o = options[idx];
|
|
160
|
+
if (o.disabled) { warn = "선택할 수 없는 항목입니다."; return; }
|
|
161
|
+
if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.name === "return" || key.name === "enter") {
|
|
165
|
+
if (required && chosen.size === 0) { warn = "최소 1개 이상 선택하세요."; return; }
|
|
166
|
+
return [...chosen];
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (result !== CANCEL) {
|
|
172
|
+
const labels = options.filter((o) => chosen.has(o.value)).map((o) => o.label).join(", ") || "(없음)";
|
|
173
|
+
r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(labels, c.dim)}`]);
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── 텍스트 입력 (Enter 확정, 빈 입력=기본값) ─────────────────────────
|
|
179
|
+
// 반환: 입력 문자열(빈 입력 시 defaultValue) 또는 CANCEL.
|
|
180
|
+
export async function text({ message, defaultValue = "" }) {
|
|
181
|
+
if (!stdin.isTTY) return defaultValue;
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
184
|
+
emitKeypressEvents(stdin);
|
|
185
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
186
|
+
stdin.resume();
|
|
187
|
+
let buf = "";
|
|
188
|
+
|
|
189
|
+
const prompt = () => {
|
|
190
|
+
stdout.write(`\r${ESC}0K`); // 줄 초기화
|
|
191
|
+
const shown = buf.length ? buf : paint(defaultValue || "", c.dim);
|
|
192
|
+
stdout.write(`${S_Q} ${paint(message, c.bold)} ${shown}`);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const cleanup = () => {
|
|
196
|
+
stdin.removeListener("keypress", handler);
|
|
197
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
198
|
+
stdin.pause();
|
|
199
|
+
stdout.write("\n");
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handler = (str, key) => {
|
|
203
|
+
key = key || {};
|
|
204
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") { cleanup(); resolve(CANCEL); return; }
|
|
205
|
+
if (key.name === "return" || key.name === "enter") {
|
|
206
|
+
cleanup();
|
|
207
|
+
resolve(buf.length ? buf : defaultValue);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (key.name === "backspace") { buf = buf.slice(0, -1); prompt(); return; }
|
|
211
|
+
// 일반 문자 (제어문자 제외)
|
|
212
|
+
if (str && !key.ctrl && !key.meta && str.length === 1 && str >= " ") { buf += str; prompt(); return; }
|
|
213
|
+
};
|
|
214
|
+
stdin.on("keypress", handler);
|
|
215
|
+
prompt();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Y/N 확인 (←→ 또는 y/n, Enter 확정) ──────────────────────────────
|
|
220
|
+
// 반환: true/false 또는 CANCEL.
|
|
221
|
+
export async function confirm({ message, initialValue = true }) {
|
|
222
|
+
if (!stdin.isTTY) return initialValue;
|
|
223
|
+
const r = makeRenderer();
|
|
224
|
+
let val = initialValue;
|
|
225
|
+
|
|
226
|
+
const draw = () => {
|
|
227
|
+
const yes = val ? paint("● 예", c.green) : paint("○ 예", c.dim);
|
|
228
|
+
const no = !val ? paint("● 아니오", c.green) : paint("○ 아니오", c.dim);
|
|
229
|
+
r.render([S_BAR, `${S_Q} ${paint(message, c.bold)}`, `${S_BAR} ${yes} ${no}`,
|
|
230
|
+
paint(`└ ←/→ 또는 y/n · Enter 확정 · ESC 취소`, c.gray)]);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = await keySession(draw, (str, key) => {
|
|
234
|
+
if (key.name === "left" || key.name === "right" || key.name === "tab") { val = !val; return; }
|
|
235
|
+
if ((str || "").toLowerCase() === "y") { val = true; return; }
|
|
236
|
+
if ((str || "").toLowerCase() === "n") { val = false; return; }
|
|
237
|
+
if (key.name === "return" || key.name === "enter") return val;
|
|
238
|
+
return;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (result !== CANCEL) {
|
|
242
|
+
r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(result ? "예" : "아니오", c.dim)}`]);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── 출력 헬퍼 (clack intro/outro/note/cancel 대체) ──────────────────
|
|
248
|
+
export function intro(text) { stdout.write(`\n${paint("┌", c.gray)} ${paint(text, c.bold)}\n`); }
|
|
249
|
+
export function outro(text) { stdout.write(`${paint("└", c.gray)} ${paint(text, c.green)}\n\n`); }
|
|
250
|
+
export function cancelMessage(text = "취소했습니다.") { stdout.write(`${paint("■", c.yellow)} ${paint(text, c.yellow)}\n`); }
|
|
251
|
+
export function note(text, title = "") {
|
|
252
|
+
const lines = String(text).split("\n");
|
|
253
|
+
stdout.write(`${paint("○", c.cyan)} ${paint(title, c.bold)}\n`);
|
|
254
|
+
for (const l of lines) stdout.write(`${S_BAR} ${l}\n`);
|
|
255
|
+
stdout.write(`${S_BAR}\n`);
|
|
256
|
+
}
|
|
257
|
+
export function log(text = "") { stdout.write(`${text}\n`); }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// 첫 화면 상태 표시 층 (#446 층2·3·5) — 감지 로그 · 분석 카드 · 신규/업데이트 판별
|
|
2
|
+
// (층5의 Breaking Changes 박스는 core/breaking-check.js가 담당.
|
|
3
|
+
// 원본의 층4 IDE Skills 상태는 project-auto-wizard 스코프 제외 — Agent Skills 미포함)
|
|
4
|
+
import { A, paint } from "./ansi.js";
|
|
5
|
+
import { markerForType } from "../core/detect.js";
|
|
6
|
+
|
|
7
|
+
const GUT = paint("│", A.gray);
|
|
8
|
+
const HEAD = paint("◆", A.cyan);
|
|
9
|
+
const OK = paint("✓", A.green);
|
|
10
|
+
|
|
11
|
+
// 층2 — 감지 로그 (.ps1 감지 진행 표시 등가)
|
|
12
|
+
export function printDetectionLog({ types = [], version = "", branch = "" }, out = (s) => process.stdout.write(s)) {
|
|
13
|
+
out(`${paint("┌", A.gray)} 🔍 프로젝트를 살펴보는 중...\n`);
|
|
14
|
+
if (types.length && !(types.length === 1 && types[0] === "basic")) {
|
|
15
|
+
for (const t of types) {
|
|
16
|
+
const marker = markerForType(t);
|
|
17
|
+
out(`${GUT} ${OK} ${marker ? `${marker} 발견 → ` : ""}${paint(t, A.bold)} 감지\n`);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
out(`${GUT} ${paint("─", A.dim)} 마커 파일 없음 → ${paint("basic", A.bold)} (직접 선택 가능)\n`);
|
|
21
|
+
}
|
|
22
|
+
out(`${GUT} ${OK} 버전: ${paint(`v${version}`, A.green)} · 브랜치: ${paint(branch, A.green)}\n`);
|
|
23
|
+
out(`${GUT}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 층3 — 프로젝트 분석 개요 카드 (.ps1 Print-ProjectAnalysis 등가+)
|
|
27
|
+
export function printAnalysisCard({ mode = "", modeLabel = "", types = [], version = "", branch = "",
|
|
28
|
+
includeNexus = null, includeSecretBackup = null, paths = new Map(), showOptional = false },
|
|
29
|
+
out = (s) => process.stdout.write(s)) {
|
|
30
|
+
out(`${HEAD} ${paint("프로젝트 분석 결과", A.bold)}\n`);
|
|
31
|
+
const row = (icon, label, value) => out(`${GUT} ${icon} ${label.padEnd(10)} ${value}\n`);
|
|
32
|
+
row("📂", types.length > 1 ? "타입(멀티)" : "타입", paint(types.join(", ") || "basic", A.bold));
|
|
33
|
+
row("🌙", "버전", paint(`v${version}`, A.green));
|
|
34
|
+
row("🌿", "브랜치", branch);
|
|
35
|
+
if (modeLabel || mode) row("💫", "통합 모드", modeLabel || mode);
|
|
36
|
+
if (showOptional) {
|
|
37
|
+
row("📦", "Nexus", includeNexus === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
38
|
+
row("🔐", "Secret백업", includeSecretBackup === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
39
|
+
}
|
|
40
|
+
// 모노레포 경로 — 루트가 아닌 항목이 하나라도 있으면 표시
|
|
41
|
+
const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
|
|
42
|
+
if (nonRoot.length) {
|
|
43
|
+
row("📁", "경로", [...paths.entries()].map(([t, p]) => `${t}→${p}`).join(", "));
|
|
44
|
+
}
|
|
45
|
+
out(`${GUT}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 층5 — 신규 통합 vs 업데이트 판별 라인 (Breaking 박스는 breaking-check.js)
|
|
49
|
+
export function printInstallKind({ currentTemplateVersion = "", templateVersion = "" }, out = (s) => process.stdout.write(s)) {
|
|
50
|
+
if (currentTemplateVersion) {
|
|
51
|
+
out(`${GUT} ♻️ ${paint("업데이트", A.bold)} — 템플릿 ${paint(`v${currentTemplateVersion}`, A.dim)} → ${paint(`v${templateVersion}`, A.green)}\n`);
|
|
52
|
+
} else {
|
|
53
|
+
out(`${GUT} 🆕 ${paint("신규 통합", A.bold)} — 이 프로젝트에 처음 설치합니다 (템플릿 ${paint(`v${templateVersion}`, A.green)})\n`);
|
|
54
|
+
}
|
|
55
|
+
out(`${GUT}\n`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// 완료 요약 출력 (.sh print_summary 등가). 전부 stderr.
|
|
2
|
+
// ctx: { mode, types:[], version, counters:{ workflows }, branches?, includeCodeRabbit? }
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { PATHS, WORKFLOW_PREFIX, WORKFLOW_COMMON_PREFIX } from "../core/paths.js";
|
|
6
|
+
import { listYamlFiles } from "../core/fsutil.js";
|
|
7
|
+
|
|
8
|
+
const SEPARATOR = "────────────────────────────────────────";
|
|
9
|
+
|
|
10
|
+
export function printSummary(ctx, targetRoot = ".") {
|
|
11
|
+
const { mode, types = [], version = "", counters = {}, branches = null, includeCodeRabbit = false } = ctx || {};
|
|
12
|
+
const err = (s = "") => process.stderr.write(`${s}\n`);
|
|
13
|
+
// 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
|
|
14
|
+
const isTty = !!process.stderr.isTTY;
|
|
15
|
+
const YELLOW = isTty ? "\x1b[1;33m" : "";
|
|
16
|
+
const CYAN = isTty ? "\x1b[0;36m" : "";
|
|
17
|
+
const NC = isTty ? "\x1b[0m" : "";
|
|
18
|
+
const workflowsCopied = counters.workflows ?? 0;
|
|
19
|
+
|
|
20
|
+
err("");
|
|
21
|
+
err(SEPARATOR);
|
|
22
|
+
err("");
|
|
23
|
+
err("✨ project-auto-wizard Setup Complete!");
|
|
24
|
+
err("");
|
|
25
|
+
err(SEPARATOR);
|
|
26
|
+
err("");
|
|
27
|
+
err("통합된 기능:");
|
|
28
|
+
|
|
29
|
+
// 모드별 체크리스트
|
|
30
|
+
switch (mode) {
|
|
31
|
+
case "full":
|
|
32
|
+
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
33
|
+
err(" ✅ README.md 자동 버전 업데이트");
|
|
34
|
+
err(" ✅ GitHub Actions 워크플로우 (AI 릴리스 자동화 포함)");
|
|
35
|
+
err(" ✅ .gitignore 필수 항목");
|
|
36
|
+
break;
|
|
37
|
+
case "version":
|
|
38
|
+
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
39
|
+
err(" ✅ README.md 자동 버전 업데이트");
|
|
40
|
+
err(" ✅ .gitignore 필수 항목");
|
|
41
|
+
break;
|
|
42
|
+
case "workflows":
|
|
43
|
+
err(" ✅ GitHub Actions 워크플로우 (AI 릴리스 자동화 포함)");
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 브랜치 모드 + 릴리스 요약 엔진 안내 (DESIGN-SPEC §4~5)
|
|
48
|
+
if (branches) {
|
|
49
|
+
err("");
|
|
50
|
+
err("브랜치 구성:");
|
|
51
|
+
if (branches.mode === "trunk-based") {
|
|
52
|
+
err(` 🌿 ${branches.main} 단일 브랜치 (trunk-based) — RELEASE-PUBLISH 하나가 버전확정→체인지로그→tag→Release를 순차 처리`);
|
|
53
|
+
} else {
|
|
54
|
+
err(` 🌿 개발 ${branches.develop} → 릴리스 ${branches.main} (pr-flow) — 릴리스 PR에서 버전확정·AI 체인지로그·automerge`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (mode === "full" || mode === "workflows") {
|
|
58
|
+
err("");
|
|
59
|
+
err("릴리스 노트 요약 엔진:");
|
|
60
|
+
if (includeCodeRabbit) err(" 🤖 1순위 CodeRabbit PR 요약 (opt-in) → AI_API_KEY → GitHub Models(무료) → 규칙 fallback");
|
|
61
|
+
else err(" 🤖 AI_API_KEY(선택) → GitHub Models(기본·무료·API 키 불필요) → 규칙 fallback — 릴리스는 절대 막히지 않음");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
err("");
|
|
65
|
+
err("추가된 파일:");
|
|
66
|
+
err(` 📄 version.yml (버전: ${version}, 타입: ${types.join(",")})`);
|
|
67
|
+
err(" 📝 README.md (버전 섹션 추가)");
|
|
68
|
+
err("");
|
|
69
|
+
err("추가된 워크플로우:");
|
|
70
|
+
|
|
71
|
+
// 실제 복사된 워크플로우 분류
|
|
72
|
+
const commonWorkflows = [];
|
|
73
|
+
const typeWorkflows = [];
|
|
74
|
+
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
75
|
+
if (existsSync(workflowsDir)) {
|
|
76
|
+
const typePrefixes = types.map((t) => `${WORKFLOW_PREFIX}-${t.toUpperCase()}-`);
|
|
77
|
+
for (const filename of listYamlFiles(workflowsDir)) {
|
|
78
|
+
if (!filename.startsWith(`${WORKFLOW_PREFIX}-`)) continue; // PROJECT-*.{yaml,yml}만
|
|
79
|
+
if (filename.startsWith(`${WORKFLOW_COMMON_PREFIX}-`)) {
|
|
80
|
+
commonWorkflows.push(filename);
|
|
81
|
+
} else if (typePrefixes.some((p) => filename.startsWith(p))) {
|
|
82
|
+
typeWorkflows.push(filename);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (commonWorkflows.length > 0 || typeWorkflows.length > 0) {
|
|
88
|
+
err(` 📦 새로 설치됨 (${workflowsCopied}개):`);
|
|
89
|
+
for (const wf of commonWorkflows) err(` 📌 ${wf}`);
|
|
90
|
+
for (const wf of typeWorkflows) err(` 🎯 ${wf}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
err("");
|
|
94
|
+
err(" 🔧 .github/scripts/");
|
|
95
|
+
err(" ├─ version_manager.py");
|
|
96
|
+
err(" └─ changelog_manager.py");
|
|
97
|
+
err("");
|
|
98
|
+
|
|
99
|
+
// 프로젝트 타입별 안내
|
|
100
|
+
if (types.includes("spring")) {
|
|
101
|
+
err(" 💡 Spring 프로젝트 추가 설정:");
|
|
102
|
+
err(" • build.gradle의 버전 정보가 자동 동기화됩니다");
|
|
103
|
+
err(" • CI/CD 워크플로우에서 GitHub Secrets 설정이 필요합니다");
|
|
104
|
+
err("");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
err(" 📖 REPO: https://github.com/Twin-Fang/project-auto-wizard");
|
|
108
|
+
err("");
|
|
109
|
+
|
|
110
|
+
// 필수 작업 안내
|
|
111
|
+
err(SEPARATOR);
|
|
112
|
+
err("");
|
|
113
|
+
err(`${YELLOW}⚠️ 다음 작업을 확인해주세요:${NC}`);
|
|
114
|
+
err("");
|
|
115
|
+
err(" 1️⃣ 릴리스 automerge용 PAT (선택 — 없으면 GITHUB_TOKEN 사용)");
|
|
116
|
+
err(" → Repository Settings > Secrets > Actions");
|
|
117
|
+
err(" → Secret Name: WORKFLOW_PAT (Scopes: repo, workflow)");
|
|
118
|
+
err(" → GITHUB_TOKEN 머지는 후속 워크플로우를 트리거하지 않습니다");
|
|
119
|
+
err("");
|
|
120
|
+
err(" 2️⃣ GitHub Actions 권한 확인");
|
|
121
|
+
err(" → Settings > Actions > Workflow permissions: Read and write");
|
|
122
|
+
err("");
|
|
123
|
+
err(SEPARATOR);
|
|
124
|
+
err("");
|
|
125
|
+
err(`${CYAN}📖 워크플로우 구성과 릴리스 흐름은 README를 참고하세요.${NC}`);
|
|
126
|
+
err("");
|
|
127
|
+
}
|